Sunteți pe pagina 1din 336

Introduction .............................................................................................................

7
Customizing ArcGIS.................................................................................................................................7

Creating a basic COM class .........................................................................................8


Creating COM components in VB ...............................................................................................................8
Creating an ATL COM Server and Object in VC++........................................................................................8

Extending ArcObjects Contents ...................................................................................8


Using Extending ArcObjects ........................................................................................9

Chapter 2: Developing Objects ..................................................................... 11

Developing Objects ................................................................................................. 11


Choosing your Development Environment................................................................... 11
Visual Basic 6 editions ...........................................................................................................................13

Useful utilities and tools ........................................................................................... 13


Useful utilities and tools .........................................................................................................................13
3rd party tools ......................................................................................................................................13
ArcGIS Developer Kit Tools.....................................................................................................................14
ArcGIS Developer Kit Addins...................................................................................................................14

Creating Objects; Coding Interfaces and Members ....................................................... 14


Coding classes, interface, and members...................................................................................................14
Defining New Interfaces .........................................................................................................................19
Coding Interface Members......................................................................................................................21

Creating Property Pages........................................................................................... 23


Property pages and property sheets ........................................................................................................24
Embedded property pages......................................................................................................................25
Property page interfaces ........................................................................................................................26
Implementing a property page in Visual Basic ...........................................................................................26
Implementing a property page in VC++ ...................................................................................................28
Displaying A Property Sheet ...................................................................................................................30

Design Guidelines for Property Pages and Dialog Boxes ................................................ 31


Design guidelines for property pages and other dialog boxes ......................................................................31
Dialog Box Units....................................................................................................................................31
Progress Indicators................................................................................................................................34

Component Categories............................................................................................. 34
COM and the registry .............................................................................................................................34
The use of ESRI component categories in ArcGIS ......................................................................................36
Methods of registering to a component category .......................................................................................36
Programming with the ComponentCategoryManager coclass .......................................................................39

Implementing Cloning.............................................................................................. 40
Copying members: Values and object References ......................................................................................41
Implementing IClone .............................................................................................................................42

Implementing Persistence ........................................................................................ 47


Persistence in ArcGIS.............................................................................................................................47
Persistable classes.................................................................................................................................48
ObjectStreams ......................................................................................................................................48
Implementing Persistence ......................................................................................................................49
Techniques for persisting different data....................................................................................................51
Version Compatibility .............................................................................................................................53
Coding backward compatibility in persistence ...........................................................................................54
Coding 'Save A Copy' functionality...........................................................................................................58
Your responsibilities when implementing persistence .................................................................................60

Creating type libraries using IDL ............................................................................... 60


About type libraries ...............................................................................................................................60

Implementing help for custom classes........................................................................ 66


Creating a help system ..........................................................................................................................66
Help in ArcGIS Desktop applications ........................................................................................................66
Invoking Compiled Help Files ..................................................................................................................67
Displaying help for your component.........................................................................................................69

Error handling in components ................................................................................... 73


How COM handles errors ........................................................................................................................73

Chapter 3: Extending the User Interface ...................................................... 78

Extending the Framework ........................................................................................ 78


Extending the User Interface .................................................................................... 78
Commands And Tools Example ................................................................................. 80
Commands and Tools Example................................................................................................................81
Creating the SampleCommand................................................................................................................81
Creating the SampleSubtypedCmd ..........................................................................................................84
Creating the SampleTool ........................................................................................................................85
Creating the SampleToolControl ..............................................................................................................87
Creating the SampleMultiItem.................................................................................................................91
Creating the SampleMenu ......................................................................................................................92
Creating the SampleToolbar ...................................................................................................................93
Creating the SampleExtension ................................................................................................................95

About Extensions .................................................................................................... 97


Just-in-time Extensions..........................................................................................................................97
Developing Custom Extensions ...............................................................................................................98

DDE Command Handler Example............................................................................... 99


DDE handler example ............................................................................................................................99
What is DDE?...................................................................................................................................... 100
Creating a DDE command handler ......................................................................................................... 101
Setting up a DDE conversation.............................................................................................................. 103

Chapter4: Creating Cartography ................................................................. 105

Creating Cartography ............................................................................................ 105


Creating Custom TOC Views ................................................................................... 106
Designing a custom TOC view ............................................................................................................... 107

TOC Catalog View Example..................................................................................... 108


Creating a tree view ............................................................................................................................ 109
Plugging CatalogView into ArcMap ......................................................................................................... 112

Creating different kinds of TOC views....................................................................... 112


Creating Custom Elements ..................................................................................... 114
Info Text Element Example..................................................................................... 116
The case for a custom Graphic Element.................................................................................................. 116
Creating the Info Text Element ............................................................................................................. 117
Calculating the text values ................................................................................................................... 118
Element activation and deactivation....................................................................................................... 120
Boundaries and outline of an Element .................................................................................................... 120
Plugging your custom element into ArcMap............................................................................................. 125
Creating a property page for the InfoTextElement ................................................................................... 127

Creating different kinds of custom Element ............................................................... 129


Creating point, line, and fill graphic elements ......................................................................................... 129

About Map Grids ................................................................................................... 130


Creating a subtype of MapGrid .............................................................................................................. 130
Design issues for a custom grid coclass.................................................................................................. 130

Clippable Index Grid Example ................................................................................. 132


The case for a custom map grid ............................................................................................................ 133
Creating the clippable index grid ........................................................................................................... 133
Implementing other kinds of custom grids .............................................................................................. 141
Plugging your custom grid into ArcMap .................................................................................................. 142
Creating a ClippableIndexGridFactory .................................................................................................... 143
Creating a property page for the ClippableIndexGrid................................................................................ 144
A User Interface for creating new custom map grids ................................................................................ 145
Layer Classes in ArcGIS ....................................................................................................................... 148

Creating Custom Layers ......................................................................................... 148


Simple Point Layer Example ................................................................................... 149
The case for a custom simple point layer................................................................................................ 150
Creating the SimplePointLayer .............................................................................................................. 151
Creating the SimplePointIdObj .............................................................................................................. 163
Layer Property Pages ........................................................................................................................... 164
Creating the SimplePointPropPage ......................................................................................................... 165
Custom GxObjects for a custom Layer ................................................................................................... 166
Creating the SimplePointLayerGxObject ................................................................................................. 167
Creating the SimplePointLayerGxObjectFactory ....................................................................................... 168
LayerFactories, Enumerations, and Names ............................................................................................. 171
Creating the SimplePointLayerFactory .................................................................................................... 172
Creating the SimplePointLayerName ...................................................................................................... 173
Creating the SimplePointEnumLayer ...................................................................................................... 175

Chapter 5: Extending the Display ............................................................... 176

Customizing the Display......................................................................................... 176


Creating custom symbols ....................................................................................... 176
Logo Marker Symbol Example ................................................................................. 178
Case for a custom Marker symbol.......................................................................................................... 178
Creating a subtype of MarkerSymbol ..................................................................................................... 179
Creating the LogoMarkerSymbol............................................................................................................ 180
Symbol Property Pages ........................................................................................................................ 188

Vertex Line Symbol Example .................................................................................. 191


Case for a custom Line symbol.............................................................................................................. 191
Creating a subtype of LineSymbol ......................................................................................................... 192
Creating the VertexLineSymbol ............................................................................................................. 192
Symbol Property Pages ........................................................................................................................ 196

Creating other kinds of custom symbols ................................................................... 198


Fill symbols ........................................................................................................................................ 198
Text symbols ...................................................................................................................................... 198
Chart symbols..................................................................................................................................... 198

Custom feature renderers ...................................................................................... 199


3

Point Dispersal Renderer Example ........................................................................... 200


The case for a point dispersal renderer .................................................................................................. 201
Creating the PointDispersalRenderer...................................................................................................... 202
Renderer property pages...................................................................................................................... 207

Managing Custom Feature Renderers ....................................................................... 211

Chapter 6: Adapting the Catalog................................................................. 212

Adapting the Catalog ............................................................................................. 212


About GxObjects and GxObjectFactories ................................................................... 212
GxObjects in ArcCatalog....................................................................................................................... 212
How GxObjects and GxObjectFactories are used...................................................................................... 213
GxObject Metadata .............................................................................................................................. 214

GxInterchangeObject and GxInterchangeFactory Example .......................................... 215


GxInterchangeObject Example .............................................................................................................. 215
The case for a GxInterchangeObject ...................................................................................................... 216
Creating a subtype of GxObject............................................................................................................. 217
Creating the GxInterchangeObject......................................................................................................... 217
Adding Metadata Support ..................................................................................................................... 223
Creating a subtype of GxObjectFactory .................................................................................................. 227
Creating the GxInterchangeFactory ....................................................................................................... 227
Plugging GxInterchangeObject into ArcCatalog........................................................................................ 228
Creating other kinds of GxObject and GxObjectFactory ............................................................................ 229
IGxObjectEdit::EditProperties ............................................................................................................... 230
Adding object caching .......................................................................................................................... 231
GxObjects with wizards ........................................................................................................................ 233
Synchronizing metadata....................................................................................................................... 234
Creating other kinds of GxObjectFactories .............................................................................................. 234

GxFilter Interchange Files Example .......................................................................... 236


The case for a GxFilterInterchangeFiles class .......................................................................................... 236
Creating the GxFilterInterchange........................................................................................................... 237

Chapter 7: Customizing the Geodatabase ................................................... 241

Customizing the Geodatabase ................................................................................. 241


About Class extensions .......................................................................................... 241
PipeValidation Class Extension Example ................................................................... 242
The case for a Pipe validation class extension ......................................................................................... 243
Implementing a class extension ............................................................................................................ 243

Managing class extensions ..................................................................................... 245


Timestamper Class Extension Example..................................................................... 246
The case for a Timestamper class extension ........................................................................................... 247
Implementing a class extension with extension properties ........................................................................ 247
Implementing a feature class property page ........................................................................................... 251

Class Extensions and Relationship Classes ................................................................ 252


Class Extensions for Annotation and Dimensions........................................................ 253
About Custom Features.......................................................................................... 254
Tree Custom Feature Example ................................................................................ 254
Implementing your own interface .......................................................................................................... 255
Handling aggregation........................................................................................................................... 255
Making your code efficient .................................................................................................................... 257

Custom Features Versus Other Solutions .................................................................. 257


Solving feature symbology ................................................................................................................... 258
Handling data edit events..................................................................................................................... 258
Overriding standard interfaces .............................................................................................................. 258
Other reasons to use custom features.................................................................................................... 260

Making a Class Extension with your Custom Feature .................................................. 260


Managing Custom Features .................................................................................... 260
About Plug-in Data Sources .................................................................................... 261
SimplePoint Plug-In Data Source Example ................................................................ 263
The case for a simple point plug-in data source....................................................................................... 263
Creating a plug-in data source .............................................................................................................. 264
Implementing a plug-in workspace factory helper.................................................................................... 264
Implementing a plug-in workspace helper .............................................................................................. 265
Implementing a plug-in dataset helper................................................................................................... 266
Implementing a plug-in cursor helper .................................................................................................... 268

Other Plug-In Data Source Topics............................................................................ 270


Plug-In Data Source Objects ................................................................................................................. 270
Implementing attribute indexes for plug-in data sources .......................................................................... 271
Implementing license handling for plug-in data sources ........................................................................... 272
Enabling ArcCatalog searches with plug-in data sources ........................................................................... 272
Custom context menus and plug-in data sources..................................................................................... 272
Improving browse performance in ArcCatalog for plug-in data sources....................................................... 273
Programmatically accessing plug-in data sources .................................................................................... 273

About Workspace Extensions .................................................................................. 274


4

Connection Log Workspace Extension Example .......................................................... 274


The case for a connection log workspace extension ................................................................................. 275
Capturing the connection event............................................................................................................. 275
Hiding data dictionary tables from users................................................................................................. 276
Implementing your own interface .......................................................................................................... 276
Workspace Property Pages ................................................................................................................... 277

Managing Workspace Extensions ............................................................................. 277


About OLE DB Providers ......................................................................................... 278
OGIS OLE DB Provider Example .............................................................................. 279
About the OGIS OLE DB provider example.............................................................................................. 280
Starting to develop an OLE DB provider ................................................................................................. 280
Implementing the Data Source object .................................................................................................... 281
Implementing the standard schema rowsets ........................................................................................... 282
Implementing the OGIS schema rowsets ................................................................................................ 282
Implementing the Session object .......................................................................................................... 283
Implementing the Command object ....................................................................................................... 283
Implementing the Rowset object ........................................................................................................... 284

Chapter 8: Extending the Editor .............................................................................. 285


Extending the editing framework........................................................................................................... 285

Using Macros........................................................................................................ 285


Macros Using the Editor ....................................................................................................................... 285
A Simple Macro Scenario ...................................................................................................................... 286
Other editing scenarios that can be solved using a macro......................................................................... 286

Editor Commands And Tools ................................................................................... 287


Editor Commands................................................................................................................................ 287
Editor Tools ........................................................................................................................................ 288

Difference Command Example ................................................................................ 289


The case for a difference command ....................................................................................................... 290
Creating an editor command................................................................................................................. 290
Creating the DifferenceCommand .......................................................................................................... 290

Split at Intersection Tool Example ........................................................................... 293


The case for a split at intersection command .......................................................................................... 293
Creating an editor tool ......................................................................................................................... 294
Creating the SplitAtIntersectionTool....................................................................................................... 294

About Edit Tasks ................................................................................................... 298


Construct Point Edit Task Example........................................................................... 299
The case for a construct point edit task .................................................................................................. 300
Creating an edit task ........................................................................................................................... 300
Creating the ConstructPointTask ........................................................................................................... 301

About Editor Extensions ......................................................................................... 303


About Snap Agents................................................................................................ 304
Subtypes Snap Agent Example................................................................................ 305
The case for a subtypes snap agent ....................................................................................................... 306
Creating a Snap Agent ......................................................................................................................... 306
Creating the SubtypesSnap Agent ......................................................................................................... 307
Plugging the SubtypesSnap agent into ArcMap........................................................................................ 310
Creating an Editor Extension................................................................................................................. 310
Creating the SnapExtension.................................................................................................................. 310
Creating the SnapDockableWindow........................................................................................................ 314
Creating the ShowSnapWindow command .............................................................................................. 314

About Custom feature inspectors............................................................................. 316


Tabbed Feature Inspector Example .......................................................................... 316
The case for a tabbed feature inspector ................................................................................................. 317
Creating a TabbedFeatureInspector ....................................................................................................... 318

Appendices................................................................................................. 322

Bibliography ......................................................................................................... 322


Object orientation ............................................................................................................................... 322
COM .................................................................................................................................................. 322
IDL.................................................................................................................................................... 322
ATL ................................................................................................................................................... 322
Visual C++ ......................................................................................................................................... 322
Visual Basic ........................................................................................................................................ 322
Windows API programming................................................................................................................... 322

Editing IDL........................................................................................................... 322


In this appendix .................................................................................................................................. 323
Editing the IDL created by OLE View for a VB component ......................................................................... 323
IDL Standards..................................................................................................................................... 328

Geodatabase modeling ............................................................................... 330

Geodatabase modeling with UML ............................................................................. 330


Creating UML object models for custom classes ......................................................... 331
The ESRI Template model .................................................................................................................... 331

Generating code.................................................................................................................................. 332


Generated Code .................................................................................................................................. 334

Introduction
The ArcGIS family of applications relies on ArcObjects to provide data management, map
presentation functionality, and more.
As the platform is Component Object Model (COM)-based, you are able to customize ArcObjects to
the lowest level. You can create your own components to plug in to the existing framework, tailoring
the platform to your specific work flow.
The key to creating such components is a thorough understanding of the ArcObjects framework and
surrounding issues. This book helps you understand how to create components for the ArcObjects
platform.
Topics covered in this chapter include advice on using this book, reasons for creating custom objects,
prerequisites, a recap of key ArcObjects and COM concepts, and getting started.

Customizing ArcGIS
The new generation of ArcGIS was designed from the ground up with extensibility in mind; not just extensibility to
allow the product to grow with successive releases, but also to allow third party users like yourself to customize and
extend the product. Because ESRI used Microsoft COM to create the ArcObjects platform, upon which ArcGIS is built,
the entire system is potentially open to customization down to a low level.
As an ArcObjects developer, you should already be familiar with the ArcGIS Developer Guides, which outline the
development options open to you. You should also be familiar with the ArcGIS Developer Help system, which details
each class and interface within ArcObjects to a developer and helps you construct effective client code.
Extending ArcObjects is aimed at developers who want to extend the core ArcObjects object models,
creating custom objects which plug seamlessly into the ArcGIS environment.
This book builds on the knowledge contained in the developer guides and the Developer Help system, aiming to show
you how these same interfaces can be implemented in your own custom objects. This approach allows you to
transparently build-in custom functionality to the ArcGIS applications, tailored specifically to your requirements.
Why create custom objects?
You may have customized the basic ArcGIS applications for one of several reasons: to automate simple repetitive
tasks, streamline your work flow, create new functionality, or produce third party solutions and add-ons to ArcGIS.
In any of these cases, you need to work out the right solution for the task by considering many issues, for example
the technically possible options for a solution to your task (there may be numerous possible solutions for each task).
You also need to consider your choice (or restrictions) of development environment and the options available for
distributing the chosen solution to your users.
Creating custom objects
When your task is defined, you may find it points naturally toward a custom object solution. For example, you may
require a symbol that is slightly different from the symbols available, or you may want to use the functionality of a
layer, but your data source is not supported. In such cases, you may decide to extend ArcGIS by creating your own
custom version of the required class or classes.
In the Introduction to COM, you were informed that COM allows ArcObjects objects to be reused at a binary level,
meaning that third party developers do not require access to source code in order to extend the system even at the
lowest level.
Objects encapsulate the manipulation methods and the data that characterizes each instantiated object behind a welldefined interface. This promotes structured and safe system development since the client of an object is protected
from knowing any of the details of how a particular method is implemented. COM does not specify how an application
should be structured; as an application programmer working with COM, language, structure, and implementation
details are left up to you. In this way, COM development is based on trust between the implementer and the user of
functionality.
The description above may raise immediate questionsyou are free to go ahead and implement interfaces and
produce classes that plug in to the existing ArcGIS framework, but the framework trusts you to implement things
correctly, even though you do not know how existing classes have implemented the functionality internally. Therefore,
you need to know some ground rules for creating new classes.
The extensible model has both benefits and drawbacks to you as a third party developer. You benefit
from the open opportunity to customize the ArcGIS framework by creating new COM objects. Your
challenge is to produce objects that behave as expected, performing the tasks the ArcGIS client
application is expecting to be done.
By using a custom class as part of your programming solution, you can achieve tight integration of your solution into
ArcGIS, because the ArcGIS application can create and use objects as it would standard objects. Your solution may
require no changes to the ArcGIS user interface (UI). Often, you will be able to provide all the required UI
customization as 'standard' looking UI additions. You may also find it useful to create custom classes if you need
to provide your new functionality to another developer rather than simply to other end users.
You will find Extending ArcObjects useful if you need to extend or customize the functionality in ArcGIS by producing
classes and applications that are intended for distribution beyond your own desktop. You will learn about these types

of customizations by the examples that form the bulk of this book.

Creating a basic COM class


The key to extending ArcObjects lies in the ability of a COM object to implement interfaces defined elsewhere. This
means that you, as a third party developer, can implement ArcObjects interfaces, allowing existing ArcObjects classes
that work with these interfaces to communicate with your custom objects.
Below is a brief summary of the procedure you might use to create a custom COM class, which implements an
ArcObjects interface in VB and VC++ with ATL.
This is not intended as a tutorial but as a brief summary of the procedure you would take to create any of the
examples in this book from scratch. If you find that there are any areas with which you are unfamiliar, it may be best
if you learn more about these areas before beginning your customization. You will find hyperlinks to relevant
information in Chapter 2, 'Developing Objects', and in other areas of the ArcGIS Developer Help. You may also find it
useful to read the subject further by using other programming resources such as those listed in the bibliography.

Creating COM components in VB


In Visual Basic you can build a COM component by creating an ActiveX Dynamic Link Library (DLL).
1.

Start VB and begin a new ActiveX DLL Project.

2.

Make sure that the Instancing property for the initial class module and any other class modules you add to the
Project is set to 5MultiUse.

3.

Reference the necessary ESRI object libraries.

4.

Using the Implements keyword, implement the required ArcObjects interfaces in your class, ensuring that you
stub out all the interface members.

5.

Add any additional code needed. You may need to define and implement your own interface.

6.

Establish appropriate Project and Class names to identify your component.

7.

Compile the DLL, and set the Version Compatibility to binary.

8.

Register your component to any appropriate component categories.

You may want to make use of the ESRI Interface Implementer Add-In.
See 'Creating type libraries with IDL', and the appendix 'Editing IDL' for more information.

You may want to make use of the ESRI Compile And Register Add-In.
See 'Component Categories' for more information.
If you need a more detailed step-by-step explanation for creating a custom component in VB, see 'The VB6
Development Environment'.

Creating an ATL COM Server and Object in VC++


The most straightforward way to create a COM server and class in VC++ is to use the ATL COM App Wizard.
1. Start VC++ and begin a new project by choosing the ATL COM AppWizard to create the basic COM server.
See the VC++ walkthrough for more information.
2.

From the Insert menu, click New ATL Object to start the ATL Object Wizard to create a new COM object. Use the
'simple object' option and an Interface type of Custom; you may also choose to support ISupportErrorInfo in the
wizard.

3.

Add #import statements to the precompiled header file stdafx.h for each of the object libraries you will require,
using the appropriate clauses.

4.

Implement the required interfaces in your class by using the Implement Interface wizard from the Class View
context menu (you may want to edit the automatically generated stub code).

5.

Add the new interface to the Interface Definition Language (IDL) definition for your class, and also add importlib
statements to import the libraries you added in step 3 to your IDL library block.

6.

Complete the implementation of your class by adding code to the members as necessary. You may need to
define and implement your own interface.

7.

If your class needs to be registered to a particular component category, add code to the class header file or the
.rgs file.

If you need a more detailed step-by-step explanation for creating a custom component in VC++, you may want to
work through the VC++ walkthrough.

Extending ArcObjects Contents


This introductory chapter shows you an overview of the aims of Extending ArcObjects and lists prerequisites for
readers.
Chapter 2, 'Developing objects', provides indepth coverage of many technical issues which are common to many areas
of custom object creation, for example object oriented programming techniques, creating objects which are clonable
and persistable, and creating custom property pages. It is not intended to provide complete technical information from
beginner level, but to provide some context for working with the examples in this book.
The main part of Extending ArcObjects presents a series of example projects, showing you how to create custom

objects for the ArcGIS framework. These examples cover a wide range of likely customization tasks you may
undertake. For each example, the structure and rationale is described with reference to particular coding issues.
Chapter 3, 'Extending the user interface', provides examples of the basic user interface custom components such as
commands and tools, and more complex examples such as dockable windows.
Chapter 4, 'Creating cartography', shows you how to create custom objects for the ArcMap environment, such as
custom layers, elements, and map surrounds.
Chapter 5, 'Extending the display', shows you how to extend the display capabilities of ArcMap by drawing features
and elements with custom symbols, renderers and colors.
Chapter 6, 'Adapting the catalog', shows you how to create custom catalog objects to allow you to browse and
investigate your own data sources.
Chapter 7, 'Customizing the geodatabase', demonstrates examples of how you can extend the ArcObjects components
that manage geographic data.
Chapter 8, 'Extending the Editor', shows you a number of ways you can plug in custom objects to the Editor
framework, creating new edit commands and tasks, and UI components.
Appendix A provides a bibliography of useful references used throughout Extending ArcObjects.
Appendix B provides advice on writing interfaces using IDL for cross-language use.
Appendix C describes how to use Computer-Aided Software Engineering (CASE) tools to model custom objects in the
geodatabase.
Throughout these chapters, in which code extracts have been edited for brevity, an ellipsis is used to indicate missing
lines of code.
Example projects
Each example is available as source code, which can be found with the other ArcObjects samples in your developer kit
installation. They are installed to the 'Extending_ArcObjects' folder, arranged by chapter.
There are some language-related limitations when implementing objects, and some examples used in Extending
ArcObjects are limited to VC++ only. More information on the reasons behind these limitations can be found in
'Development environments for custom components' in Chapter 2.
Technical details that apply to any language implementation are generally described with reference to VB example
code throughout this book, as it is expected that VC++ programmers should be able to interpret this more readily than
VB programmers could interpret VC++. Details specific to a particular language will be described with reference to
code examples in that language.
User-defined interfaces on examples in this book
Each example in this book implements one or more interfaces defined in the ESRI object libraries. However, to add the
user-defined functionality required by each example, many examples also include user-defined interfaces. For VC++
classes, COM members can only be defined by use of an interface. For VB classes, an interface is generally defined in a
separate class and implemented in the target class.

Using Extending ArcObjects


Extending ArcObjects builds on the knowledge gained from previous ArcObjects and development experience. It will be
worthwhile reading the following sections to ensure you have the knowledge that is assumed throughout the book.
ArcObjects Programming Prerequisites
Extending ArcObjects is aimed at the ArcObjects developer who is comfortable with the ArcObjects platform. You
should have experience in writing macros in Visual Basic for Applications (VBA) with ArcObjects. The ArcGIS Desktop
Developer Guide is an excellent basis for gaining the required knowledge, although topics particularly relevant to
custom objects will be reiterated in this book.
It is assumed that you have a basic knowledge of COM, being familiar with concepts such as COM classes and
interfaces, IDL, and binary compatibility. Ideally, you should be familiar with all the concepts reviewed in the
Introduction to COM.
It would also be an advantage if you have worked with an external development environment, such as Visual Basic
(VB) or Visual C++ (VC++), with or without ArcObjects. Some experience creating your own classes and defining your
own interfaces would be beneficial.
You can find useful references covering these topics in the bibliography. You may also like to consider a training course
in your chosen programming language.
Languages used in Extending ArcObjects
The discussion in this book focuses on the Visual Basic and Visual C++ 6 environments. Custom objects can generally
be created in any COM-compliant language, although as ESRI created ArcObjects components using VC++, this
language is tightly integrated with the ArcObjects libraries. VB is the most common language for third party developer
use of ArcObjects, having the widest developer base, and therefore, samples and discussion are centered around VB.
It is assumed that a VC++ developer will be able to read and interpret the simpler syntax of VB.
For reasons explained in Chapter 2, 'Developing objects', your choice of development environment may be limited by
the type of customization you want to undertake. Therefore, the discussion throughout does not take each language in
turn; instead examples and their issues are discussed with reference to both VB and VC++.

Platforms
Throughout Extending ArcObjects, it is assumed you will be creating customizations for the ArcGIS Desktop on the
Windows platform; example code projects are written with these assumptions in mind. This focus helps to keep the
examples as straightforward and understandable as possible while still providing useful functionality and
demonstrating a wide range of interface implementations. For this reason also, the examples do not cover
development environments other than VB 6 or VC++ 6, although much of the general information found in Chapter 2
and the appendixes may provide useful information to developers using other environments.
In many cases it may be possible to adapt the given code examples to work successfully on other platformsfor
example, you may want to create a custom layer which can be installed for use in an ArcGIS Server environment. In
such cases, you would need to refer to the ArcGIS Engine Developer Guide and the ArcGIS Server Developer Guide for
general advice on customization within ArcGIS Engine and ArcGIS Server.
Getting the most out of this book
The examples presented throughout this book are not detailed walkthroughs of the exact steps required to create each
customization. Rather, they use the order in which a programmer may attempt the customization and describe the
main actions and issues of the design process, which lead to the final example project.
You will get the best out of this book if you work through each example with the developer help in front of you, so you
can see information for clients of the methods you are reading about implementing and look up details of object
models, interfaces, and so on.
Many of the examples described throughout this book create classes that are similar to existing ArcObjects classesfor
example, renderers, elements, symbols, and map grids.
In these cases before you begin creating your custom class, it will be worthwhile to work in detail with the existing
ArcObjects class to see how it behaves. Try instantiating the class and reviewing the default values of any properties,
call the methods and set the properties of the class, look at the user interface options available for the class, and use
any tools in ArcGIS applications that work with the class.
This experience will help you to create your own custom object that fits with the expected behavior for such a class
for example, all IFillSymbol coclasses have a default Outline, which is a SimpleLineSymbol with a width of 1.
Before doing any programming it is important to review the ArcObjects documentation to see how
your custom class fits into the software. The object model diagrams are a good place to start, since
they provide a general overview of the objects without being cluttered by implementation details.
Terms and references used throughout this book
Some terms may be found in the text in both lowercase and capitalized forms. In this case, the lowercase form is used
to refer to a general concept or item, and the capitalized form refers to a specific application of the general concept.
For example, "property pages" refers to the concept of a form window which can be used to allow users to view and
change the properties of an object. "Property Pages" however refers to a specific set of property pages for an object.
In many cases, terms with a specific definition in the context of this book are included in the glossary.
Where other publications are referenced throughout the text, you can find full reference details in the bibliography.

10

Chapter 2: Developing Objects


Developing Objects
When you begin to develop custom objects for the ArcGIS framework, you may find that many of your customizations
deal with the same programming issues. If you are familiar with these common software development issues and
techniques, you will be able to develop your components more effectively. This chapter describes some of the more
common tasks you will perform and issues you may encounter. Generic programming issues are considered, as well as
issues specific to working with the ArcObjects object model.
You may find the information in this chapter useful once you have drawn up your requirements, before you begin
coding your components. You may also want to refer to this chapter throughout your development cycle.
Note that all the discussions focus mainly on VC++ and VB6.
Choosing a Development Environment
Advice on deciding which development environment is more suitable.
Creating Objects; Coding Interfaces and Members
Discussion of different aspects of interface and member implementation.
Creating Property Pages
Implementing property page classes.
Design Guidelines for Property Pages and Dialog Boxes
Standards and best practice for designing property pages and dialog boxes.
Component Categories
Information about what component categories are and how you can use them.
Implementing Cloning
How to create a clonable class.
Implementing Persistence
How to create a persistable class.
Type Libraries and IDL
Information about type libraries, IDL, and language compatibility.
Implementing Help for Custom Classes
Creating a help system for your component.
Error Handling in Components
Information about ArcObjects error handling, and error handling in your own components.

Choosing your Development Environment


Development Environments in Extending ArcObjects
To consume the ArcObjects object libraries and create customizations for ArcGIS, you can use any COM-compliant
development environment. You may have developed your customizations using the development environment with
which you are most familiar, but you should be aware that different development environments offer different
advantages and disadvantages. To summarize, for beginner to advanced ArcObjects programmers, VB is an ideal
environment, offering rapid application development (RAD) and simple syntax. However, this simplicity is achieved at
the price of certain limitations (as you will see throughout this chapter). VC++ requires the practice of much more
rigorous programming, and it is therefore recommended that VC++ development with ArcObjects is best achieved with
several years of VC++ experience.
The customizations explored in this book focus on the creation of custom components, extending the object models of
ArcGIS. For this type of development you will need to consider your choice of development environment carefully, as
your options may be constrained based on the type of development you want to undertake.
The choice of development environment is more critical when extending the ArcGIS object models,
compared to script-type customizations.
Visual Basic 6 and Visual C++
Many of the examples described throughout Extending ArcObjects are possible in a number of development
environments. This book concentrates on the two environments that are used by the majority of ArcObjects
developers, Visual Basic 6, and Visual C++.
(Throughout this book, 'VB' refers to Visual Basic version 6. The abbreviation VB.NET may be used for information
relating specifically to Visual Basic .NET).
Choosing a development environment based on your design requirements
The ArcObjects libraries were written in VC++. The interfaces in these libraries, through which all communication with
COM objects is performed, were defined using IDL. Therefore, all the features supported by both Visual C++ and IDL
may be found in ArcObjects interfaces.

11

The interfaces in ArcObjects were defined with IDL.


Although COM is a binary standard and, therefore, largely language-independent, differences in the features supported
by the common development environments result in certain limitations when creating custom components. Before you
start coding, you need to decide which development environment you require for your project.
Below are three main requirements that may affect your choice, you must be able to create a COM class, you may
possibly need to inherit from another class, and you must be able to implement the required interfaces.
COM class creation
Your first step to extending the ArcObjects model is to create your own COM class. Most object-oriented development
environments allow the creation of classes, for example, VB, VC++, Visual Basic for Applications (VBA), Visual Java
++ (VJ++), C++ Builder. However, not all such classes are COM classes.
In VB, class modules that are part of an ActiveX DLL or ActiveX EXE are COM classes. In Visual C++ COM classes can
be created using the ATL object wizard, or a class can be manually defined to conform to the rules of COM.
Most modern object oriented development environments allow you to create COM classes and
implement interfaces. However, different development environments have different capabilities;
these differences may affect the environments you can use to implement your custom objects.
Even VBA classes are actually COM classes; however such classes are not publicly creatable (VBA is a scripting
environment; code cannot be compiled, and components cannot be accessed by other processes), making VBA
generally unsuitable for the creation of custom components.
It is also possible to create classes in Visual Basic .NET (VB.Net) and Visual C# .NET (C#), which can act as COM
classes via the .NET-COM interop bridge. For more information on creating custom classes using .NET, see the .NET
section of the ArcGIS Developer Help (in the Development Environments section), where you can find walkthroughs for
creating custom Command and DockableWindow classes in VB.NET and C#.
Class inheritance (aggregation)
Class inheritance is an object-oriented technique for customizing or extending existing classes. It is sometimes known
as implementation inheritance or aggregation. When one class aggregates another, this class then exposes all the
interfaces and members available on the aggregated class.
One benefit of aggregation is that you can pass an instance of the new class to clients that are expecting the original,
aggregated class. In this way, you can add functionality, without needing to know all the inner workings of the
aggregated class. Some of the examples described in Chapter 8 of this book demonstrate the use of this technique.
VB does not support aggregation, and is therefore excluded from your choice of development environment in these
cases; VC++ does support aggregation. Aggregation is used to develop custom features in Chapter 7 of this book.
Interface implementation
COM classes communicate through interfaces, regardless of the underlying coclass. By implementing the interfaces
that the framework is expecting to find, existing ArcObjects components interact with your custom components
without being aware that the components are not part of ArcGIS.
All COM-compliant environments support interface implementation in some way. However, not all environments
support all the features possible in IDL; therefore, different environments will have different abilities to implement
particular interfaces.
Both the COM conventions and the IDL specification were based on the C language; therefore, C-based languages
naturally support the widest range of IDL features. If you are developing with Visual C++ 6.0, you will be able to
implement all the interfaces ArcObjects exposes, as you are using the same environment and compiler with which they
were created. Other C-based environments may have slightly different capabilities.
If you are developing with Visual Basic, you will be able to implement the vast majority of the interfaces in ArcObjects.
It is easy to quickly check if you can implement an interfaceadd a reference to the appropriate DLL, and add the
Implements line to the code window.
If the interface name does not appear in the wizard bar's left pulldown list, you may have a problemabove you can
see that ISymbol can be implemented in VB, but ISimpleLineSymbol cannot. Note that syntax errors in the code
window may prevent the wizard bar from working correctlyyou may want to check for syntax errors first before
assuming the interface cannot be implemented in VB.

Using the wizard bar, you can easily check if an interface is implementable in VB.

12

For some interfaces that cannot be implemented in VB, ArcObjects includes VB-friendly equivalents. For example, the
IPropertyPage interface definition contains the SetObjects method, which has a parameter of unsigned integer data
type. The VB environment has no equivalent for an unsigned integer, and therefore IPropertyPage cannot be
implemented in VB. In this particular case, ArcObjects provides the VB-friendly IComPropertyPage interface to do the
same job as IPropertyPage, allowing the VB developer to implement a property page.
If you want to know more about exactly what makes an interface implementable or not in VB, see the 'Creating type
libraries using IDL' section in this chapter.

Visual Basic 6 editions


If you are planning to develop with VB, it is worth noting that this product is available as three different editions:
Enterprise, Professional, or Learning editions.
All of these editions contain the same VB functionality and editor and may be used to create COM classes; therefore,
they are suitable for creating classes for ArcGIS. However, each edition has different tools and utilities. It is
recommended that you use the Professional or Enterprise version. Note that if you intend to use the Visual Modeler
tool, you require the Enterprise version.
Topics relevant to particular environments
This book is focused on the design of solutions to particular programming problems, not on one particular development
environment or another. Although many issues are dealt with differently by different environments, it is often the case
that your general understanding of the issue is helped by understanding more than one environment-specific view.
In some cases where an interface cannot be implemented in VB, ArcObjects provides an alternative
interface.
For this reason, programming issues discussed in this book are taken in turn, with reference to particular
environments where appropriate. You should therefore review an entire topic, regardless of the environment you are
using.
If you are developing in VB, you should find that you learn more about the background of an issue and the things that
are hidden by the VB compiler.
If you are developing in VC++, you should find that this helps you to design components which can be used more
effectively in other development environments, for example, VB or scripting environments.

Useful utilities and tools


Useful utilities and tools
The examples shown throughout Extending ArcObjects use a number of tools and utilities to create custom
components for ArcGIS. You may want to check that you have access to these or equivalent tools.

3rd party tools


MIDL Compiler
The Microsoft Interface Definition Language (MIDL) compiler is a utility that turns an IDL file into a type library. This
utility ships with Microsoft Visual Studio 6.0 and Visual C++ 6.0, and is used by all ArcGIS VC++ samples. It is also
used in some of the VB examples in this book.
If you do not already have this utility, it is included in the Microsoft Platform SDK Build Environment, which you can
download from the Microsoft Web site.
OLE View
The OLE View utility can be used to view type library information stored in a type library file (.tlb), an object library file
(.olb), or stored inside a DLL. It can also be used to engineer IDL code from such files.
OLE View is available as part of Microsoft Visual Studio 6.0 and Visual C++ 6.0. It is also available as part of the
Microsoft Platform SDK Build Environment, which you can download from the Microsoft Web site.
Dependency Walker
Dependency Walker is a tool that allows you to trace the DLL dependencies of your component.
This tool is available as part of the Win32 Platform SDK, installed with Microsoft Visual Studio 6.0. It is also available
from the standalone Microsoft Visual Basic 6.0 product CD. The latest version is generally available from the original
developers Web site, http://www.dependencywalker.com.
GUIDGEN
GUIDGEN is a utility that ships with Microsoft's Visual Studio 6.0, and can also be used to create a Globally Unique
Identifier (GUID), written in a variety of formats suitable for cutting and pasting to VC++ source code.
RegClean
RegClean is an unsupported Microsoft utility that can be used to remove obsolete keys in the HKEY_CLASSES_ROOT
hive of the system registry. It removes keys that reference DLLs no longer present on the system.
Although this is no longer available from Microsoft, you may be able to find this on third party Web sites. It is not

13

compatible with Windows XP or ME, but you can run this utility on Windows 95, 98, 2000, and NT 4.

ArcGIS Developer Kit Tools


Full details on the ESRI utilities included in the developer kit can be found in the Developer Tools section of the ArcGIS
Developer Help. Some of these tools that may be particularly useful for developers creating custom objects and are
highlighted below.
GUID tool
GUID tool is a standalone utility that can be used to create a new GUID and a new component category. You can use
this as an alternative to the GUIDGEN tool, although VC++ programmers may find the formats available with
GUIDGEN to be more efficient.
Register In Menu
Both VB and VC++ automatically register DLLs when they are built. However, for testing purposes you may find it
useful to be able to register and unregister DLLs using the context menu in Windows Explorer.
To install the registration utility, right-click on the Register_In_Menu.reg file and click Merge from the context menu.
After the utility has been installed, you can right-click on any .dll or .ocx file, and from the context menu, click
Register or UnRegister to register or unregister the selected files using RegSvr32.exe. You also have the option to
perform the registration without displaying a success or failure status message.
Library Locator
This standalone utility can be used to quickly find out which library contains a particular interface. It is independent of
development environment.
Object Browser
The standalone ESRI Object Viewer can be used to view the contents of type libraries and object libraries. The
declarations can be viewed as IDL, as they would appear on an object diagram, or using VB syntax. See the Creating
type libraries using IDL section later in this chapter for more information about this and other similar utilities.

ArcGIS Developer Kit Addins


Full details on the developer environment add-ins available as part of the developer kit can be found in the Add-Ins
section of the ArcGIS Developer Help. Some add-ins are particularly useful for developers creating custom objects;
these add-ins are described below.
VB6 Interface Implementor
This VB6 addin provides a quick way of implementing an ESRI interface by stubbing out all the members for a selected
interface automatically. See the Implementing Interfaces section later in this chapter for more information.
VB6 Compile and Register Add-in
This add-in for VB6 allows you to automatically add classes to component categories when a project is compiled. See
the Component Categories section later in this chapter for more information.
.NET Component Category Registrar
This Visual Studio .NET add-in allows you to quickly add code to register your classes to component categories. The
add-in uses the .NET utility classes and adds a section of code to perform the component category registration
automatically when the server is registered on the machine.

Creating Objects; Coding Interfaces and Members


Coding classes, interface, and members
Creating Classes
Many ArcGIS components, and also many of the examples presented throughout this book and your own custom
components, will revisit the same concepts of object oriented programming. They will also re-use the same design
patterns. In this section a number of common issues of class design and implementation are reviewed. The following
sections give further help on specific issues of interface implementation.
For more help on design patterns, you should read Design Patterns: Elements of Reusable Object-Oriented Software.
Although the Design Patterns book uses examples in C++ and Smalltalk, it takes a generally language-neutral
approach and is relevant to all developers of object-oriented software. VB programmers may also find it useful to refer
to Microsoft Visual Basic Design Patterns, which discusses implementing many of these design patterns specifically in
VB. Full reference details can be found in the bibliography.
Containment
Containment is a simple form of binary reuse, where an outer object contains an instance of an inner object.
Containment allows modification of the original object's method behavior, but not the method's signature. With
containment, the contained object (inner) has no knowledge that it is contained within another object (outer). The
outer object must implement all the interfaces supported by the inner to perform the same duties in the system. When
requests are made on these interfaces, the outer object simply delegates them to the inner. To support new
functionality, the outer object can either implement one of the interfaces without passing the calls on or implement an

14

entirely new interface in addition to those interfaces from the inner object.
Containment is a useful technique for implementing a custom version of an existing class by instantiating one (or
more) coclasses inside the new outer class and passing most requests straight to the contained object. However,
particular functions you want to override can be dealt with in the containing class. See the ClippableIndexGrid in
Chapter 4 for an example of containment.
Aggregation
COM aggregation involves an outer object that controls which interfaces it chooses to expose from an inner object.
Aggregation is useful when the outer object wants to delegate every call to one of its interfaces to the same interface
in the inner object. Aggregation does not allow modification of the original object's method behavior. The inner object
is aware that it is being aggregated into another object and forwards any QueryInterface calls to the outer
(controlling) object so that the object as a whole obeys the laws of COM. To the clients of an object using aggregation,
there is no way to distinguish which interfaces the outer object implements and which interfaces the inner object
implements.
One benefit of aggregation is that you can pass an instance of the new class to clients that are expecting the original,
aggregated class. In this way, you can add functionality without needing to know all the inner workings of the
aggregated class. Some of the examples described in Chapter 7, 'Customizing the Geodatabase', demonstrate the use
of this technique; for instance, the technique is used to create custom features. Visual Basic 6 does not support
aggregation, so VB developers cannot create custom features.
Singletons
Singletons are found throughout the ArcGIS object model. A singleton is a class that can only have one instance per
process or thread. ArcGIS uses the Singleton-per-thread model. Singletons are useful when many clients require a
reference to the same data. They can be used instead of class-level methods to provide a meeting point for client
code. Implementation of a singleton, however, can be tricky to achieve.
Although there are no examples of customization that include a singleton in this book, it is possible you may include a
class of this nature in a customization of your own design. With VC++ you can use an ATL macro to make your class a
singleton. However, there are some issues with singletons implemented by this method; you should investigate the
issues thoroughly via other sources, such as VC++ documentation, before attempting to create a Singleton, being
careful to account for the singleton-per-thread model. There is no inherent support for VB developers to create a
singleton object.
Non-creatable Classes
Some ArcObjects cannot be created using CoCreateInstance or by using the New keyword in VB, as they are noncreatable. Non-creatable classes are typically instantiated by the component itself and returned through a helper
function on a creatable object. This is sometimes referred to as the factory design patternit gives the component
some control over the circumstances in which the object is created and initialized. For example, ArcObjects uses this
model extensively throughout the GeodatabaseCursor, SelectionSet, and FeatureClass are all examples of noncreatable classes.
Although you can define non-creatable classes as shown below, think carefully about your reasons for doing so. Your
class cannot be cocreated by any client, and this may cause errors in methods that expect to be able to create your
class. You may experience problems with persistence, or if you register the class to a component category.
Defining a non-creatable class in VB
In VB, create your class as usual, but set the class modules Instancing property to PublicNotCreatable. Add a public
class to act as a factory with a public method to return an instance of the non-creatable object.
1.

Create a new ActiveX DLL project with two class modules, and name it, for example, MyLibrary.

2.

The first class module will be your non-creatable classgive it a name, for example, MyClass.cls, and set the
Instancing property to PublicNotCreatable.

3.

The second class module will be your helper (factory) classname the module, for example, MyFactory.cls, and
set the instancing to MultiUse. Also, add a method to access the non-creatable class.
[Visual Basic 6.0]

Public Function GetClass() As MyClass


Set GetClass = New MyClass
End Function
4.

Clients wanting to access the non-creatable class instantiate the factory object and call the GetClass method.
[Visual Basic 6.0]

Dim pFactory As New MyLibrary.MyFactory


Dim pClass As MyLibrary.MyClass
Set pClass = pFactory.GetClass
Defining a non-creatable class in VC++
A VC++ developer can implement a non-creatable class using the same design principles as a VB developer. Follow
the steps described below.
1.

In the IDL for the non-creatable class, add the noncreatable attribute.
[
uuid(2C612928-9912-47E3-B2C0-8F0FD1C1A68D),

15

helpstring("My non-creatable class"),


noncreatable
]
coclass NonCreate
{
[default] interface IUnknown;
interface IMyInterface;
};
2.

Change the object map macro for the class as shown from OBJECT_ENTRY(CLSID_NonCreate, CNonCreate) to
OBJECT_ENTRY_NON_CREATEABLE(CNonCreate).

3.

Provide a function to return a pointer to this class. You can use C++ class methods to initialize or use the non
creatable class as shown below.
[Visual C++]

CComObject<CNONCREATE>* pNonCreate = 0;
IMyInterfacePtr ipMyInterface;
// Class is noncreatable - so create locally
hr = CComObject<CNONCREATE>::CreateInstance(&ipMyInterface);
// Note object created on heap with 0 ref count
if (SUCCEEDED(hr))
{
pNonCreate ->AddRef();
// Call any C++ class initialization e.g. using pNonCreate ->Init();
hr = pNonCreate->QueryInterface(IID_IMyInterface,
(void**) &ipMyInterface);
// Keep object while smart pointer in scope
pNonCreate->Release();
}

// Use C++ class via pNonCreate while smart pointer is live

VC++ programmers should be aware of the issues with noncreatable classes that are registered to component
categoriessee the ATL Internals book referenced in the bibliography for more information.
It is also possible to remove the class entirely from the registryonly do this if you are sure that the registry entry can
be removed safely. Consider issues such as helpfile linking, and any method call that needs to cocreate your class. If
you do need to remove the registry entry entirely, remove the registration file (.rgs) for the class from the VC++
project, and change the registration in the class header file from DECLARE_REGISTRY_RESOURCEID(IDR_NONCREATE) to
DECLARE_NO_REGISTRY().
Enumerators
Enumerators are classes that provide a collection of references to other objects; for example, the IColorRamp::Colors
property returns an enumeration of Color objects. In some of the examples in this book, enumerator classes are
created to return a value that needs to be an enumerator. This is done by implementing the required enumerator
interface (enumerator interfaces generally begin with IEnum). See the ClippableIndexGrid, SimplePointLayer, and
ConnectionLog topics for examples of custom enumerator classes.
When using enumerations in client code, you do not know how the object has been implemented.
The object may create and fill a new enumeration each time one is requested, or it may, for efficiency, have been
implemented to, return a reference to a previously created enumerator, in which case the position of the enumerator
may not be at the first position. You should always, therefore, call the Reset method of an enumerator after you
receive the reference, before using it in your code.
Coding Interfaces
From your experience of programming with ArcObjects, you should be familiar with the basic concepts of the COM
interface-based programming model.
When you begin to create custom components for a COM system, you may find you need to dig a little deeper into the
concepts of how interfaces are defined and used, particularly if you are developing in VB or developing in one
development environment with your components being consumed in another environment.
You may find it useful to begin by reviewing the brief definitions of key concepts, such as the IUnknown interface and
how to implement existing inbound interfaces, before moving on to the issues of outbound interfaces and defining new
interfaces.
If you require introductory information about COM, and about how to program with an interface-based model, see the
Introduction to COM in the ArcGIS Developer Help system, as this basic information is not covered in detail in this
book. You may also want to refer to the books listed in the bibliography section for more detailed information.
Concepts of Interface-based programming
In COM, all communication between COM clients and servers is via interfacesabstract definitions, which contain no
implementation code. Programming with interfaces hides the details of a COM server implementation from a COM
client. Objects can therefore be reused at a binary level, which means you do not require access to source code,

16

header files, or object libraries in order to extend the system even at the lowest level.
All COM interfaces inherit from the IUnknown interface, therefore all COM objects indirectly implement IUnknown.
Interfaces that inherit directly from IUnknown are sometimes known as custom interfaces. The AddRef and Release
methods are used together to control object lifetime. If you are programming in VB, AddRef and Release are called
automatically by the VB garbage collector as required. VC++ programmers can avoid much use of AddRef and Release
by using smart pointers (see the Smart Types section of the Visual C++ section of the ArcGIS Developer Help system.
The QueryInterface method provides the functionality to access any interface, and therefore any interface member,
available on a class from any existing interface reference. This process is sometimes known as a QI. VB programmers
do not need to directly access IUnknown to perform a QI.
Inbound Interfaces
It is likely that the majority of the interfaces you implement on your class are existing ArcObjects inbound interfaces,
particularly if you are creating a subtype of an existing ArcObjects class. The ArcGIS client knows about these
interfaces and can use them to make calls to your class. Below is a brief review of how to implement inbound
interfaces.
Implementing inbound interfaces in VB
In VB, indicate that a class implements an inbound interface by using the Implements keyword.
[Visual Basic 6.0]

Implements ICommand
Note that the method for implementing, or sinking, an outbound interface is considerably different from implementing
an inbound interface in VB and is discussed later in this section. Remember:

You can only implement interfaces whose definition is supported by VB. This includes parameter
attributes, data types, and other issues. See the later section,

Creating type libraries with IDL, for more information.

All members of every inbound interface must be stubbed out.

Members with no actual implementation should return the appropriate error code, in this case
E_NOTIMPL. For more information on error codes, see the Error Handling section later in this
chapter.

Implementing inbound interfaces in VC++


In VC++, you declare you are implementing an interface by including it in the list of base classes from which your
class will inherit.
[Visual C++]

class ATL_NO_VTABLE CMyClass :


public CComObjectRootEx<CCOMSINGLETHREADMODEL>,
public CComCoClass<CMYCLASS, &CLSID_MyClass>,
public ICommand
{
...
}
This works because an interface in C++ is defined as a structure, so you can derive a class from an interface in the
same way as deriving a class from a structure.
You should also add the interface to the ATL COM Map section of your class declaration.
The COM Map macros expand to provide an implementation of QueryInterface() for you:
BEGIN_COM_MAP(CMyClass)
COM_INTERFACE_ENTRY(ICommand)
END_COM_MAP()
Your class declaration must also contain a prototype for each member of the interface:
[Visual C++]

STDMETHOD(get_Enabled)(VARIANT_BOOL* Enabled);
STDMETHOD(get_Checked)(VARIANT_BOOL* Checked);
STDMETHOD(get_Name)(BSTR* Name);
....
Implement each member of the interface in the implementation file of your class:
[Visual C++]

STDMETHODIMP CMyTool::get_Name(BSTR* Name)


{
if (0 == Name)
return E_POINTER;

17

// Set the internal name of this command. By convention, this


// name string contains the category and caption of the command.
*Name = ::SysAllocString(L"DeveloperSamples_MyTool");
return S_OK;
}
Outbound interfaces
So far we have dealt with inbound interfaces, in which the client calls the server component. For outbound interfaces
however, the server object calls the client.
Outbound interfaces are analogous to callbacksa mechanism that should be familiar to VC++ developers. The
methods on an outbound interface will be familiar to VB developers as events.
An object that calls the members of an outbound interface is said to be a source; an object that receives the calls from
the source is said to be a sink.

Outbound interfaces are defined in the same way as inbound interfaces, but its members are coded to present
information to a client, which it may need to know as certain events occur. For example,
IActiveViewEvents::AfterDraw has parameters specifying the display and the current phase that is being drawn.
Outbound interfaces are also implemented in a different way.
This model is inherently more complex than the inbound interface model, and the difference between using an
outbound interface in VB versus VC++ is significant. As an experienced ArcObjects programmer, you should already be
familiar with sinking outbound interfaces, but brief details of how to sink an outbound interface are described below for
both VB and VC++ before descriptions of sourcing outbound interfaces.

Sinking outbound interfaces (responding to events) in VB

In VB, outbound interfaces are sinked by using the WithEvents keyword. This mechanism should be familiar to any
ArcObjects programmer. Sinking an outbound interface may be required in any ArcGIS customization, and is not
specific to creating custom ArcObjects components. If you are unsure of how to sink an outbound interface, refer to
the Visual Basic documentation in the ArcGIS Developer Help system.

Sourcing outbound interfaces (raising events) in VB

Unfortunately, due to VBs event handling mechanism, you cannot be the source of an existing outbound interface.
There is no facility in VB for sourcing existing events interfaces, as the VB compiler creates an outbound interface
'behind the scenes' and adds all events defined in the class to that interface as methods.
This means you cannot create a class that raises events from any existing ArcObjects outbound interfaces such as
ILayerEvents, IMapFrameEvents, and so forth.
You can define new events that your class may raise using the Event keyword. In your class methods, you then raise
the event as required using RaiseEvent. VB creates a hidden outbound interface for you at compile time. All the
events you declare are placed in this hidden interface; the name of the interface is the class module name preceded
by two underscore characters. If you investigate your DLL with OLE View, you will how the outbound interface is
defined using the [source] attribute.
VB clients can sink your event by using the WithEvents keyword. The sink method the client has defined will be
called when your class raises the event.

Sinking outbound interfaces in VC++

In VC++, outbound interfaces are typically sinked by using the connection point mechanism to register its interest in
the events of a source object. Again, sinking an outbound interface may be required in any ArcGIS customization,
and is not specific to creating custom ArcObjects components. If you are unsure of how to sink an outbound
interface, refer to the Visual C++ documentation in the ArcGIS Developer Help system, in particular the 'Handling
COM Events in ATL' topic.

Sourcing outbound interfaces in VC++

In VC++, for an object to be a source of events (that is, to implement an outbound interface) it will need to provide
an implementation of IConnectionPointContainer and a mechanism to track which sinks are listening to which
IConnectionPoint interfaces. ATL provides this through the IConnectionPointContainerImpl template. Additionally, ATL
provides a wizard to generate code to fire IDispatch events for all members of a given dispatch events interface.
Details of this process can be found in the topic 'Handling COM Events in ATL' in the Visual C++ documentation
section of the ArcGIS Developer Help system.

18

Defining New Interfaces


In many of the examples in this book, coclasses require additional public properties and methods, in addition to those
available on the implemented ArcObjects interfaces.
For example, the ConnectLog example in chapter 8 requires a method to allow a client to enumerate current
connections. In such cases, a new interface has been defined and implemented by the new class. This gives the
custom component the familiar benefits of the interface-based programming modelflexibility to adapt components
being a major advantage.
Defining interfaces in VC++ using IDL
If you are developing in VC++, you should be familiar with the process of creating new interfaces explicitly using IDL,
as this is the only way to add COM functions to your classes.
VC++ developers may, in any case, find it useful to review the information in the 'Creating Type Libraries with IDL'
section later in this chapter. This section gives advice on IDL standards, helping you define interfaces suitable for use
by a variety of clients, particularly those written in VB. Note also that the syntax you use to define your interface is
dependent upon the choice of a dispatch or custom interface.
Defining interfaces in VB using a class module
The VB compiler automatically creates a new interface for each class in an ActiveX projectthis interface will contain
all the public members you defined on your class. The interface is hidden by the VB environment when you use the
class in another component, and the public members appear as if they are directly implemented on the class.

You can also use VB to define a new interface explicitly by using a new class module.
1.

Add a new class module to your component and set the Instancing property to PublicNotCreatable, as you do not
want clients to be able to directly instantiate your interface.

2.

Following the convention for interface names, set the name to begin with I, for example, IMyInterface.

3.

Add public methods and properties as required to the module.

4.

In another class module, implement the interface as you would any other interface by using the Implements
keyword.

5.

Ensure all members of the new interface are stubbed out in the implementing class.

The class module you defined does not actually define a proper COM interface, instead you take advantage of the fact
that the VB compiler automatically creates an interface for each class.

As you can see from the diagram above, the actual interface names differ from the names you use in VB. If you intend
the interface to be used from any other environment apart from VB, you should review the details in the 'Creating
interfaces with IDL' section.
This describes how you can use VB and IDL to define COM interfaces in a separate type library. Note that you cannot
use the information contained in an IDL file to define an object (for example, its name and the interfaces it
implements) for a VB class. However, a VB component can make use of enumerations defined in IDL, as long as they
use a VB-compatible data type.
Defining new outbound interfaces
You can define new outbound interfaces in IDL exactly the same way as you define inbound interfaces, as the
difference lies in the way the interface is used. However, you will need to think about the kind of information the sink
objects will need to knowwhat changes may occur to your class, what sink objects will need to know about those
changes, and which other objects your changes will affect. For example, IActiveViewEvents::AfterDraw is called many
times in succession as a view is refreshed, each time a different phase of the refresh is indicated by the phase
parameter, making this a flexible event to implement and use.

19

As noted previously, it is not possible to create a new external outbound interface and implement this in VB, as VB's
event model hides outbound event implementation details.
Default interfaces
All COM classes have a default interface specified at the type library level. The default interface is returned when a
COM object is instantiated with no interface being specified. The default interface on a class was originally intended to
be the interface that most closely represents the underlying class, providing its default functionality.
This use of default interfaces may have changed somewhat, in particular for ArcObjects classes that split essential
functionality between more than one interface.
VC++ mappings are not affected by the default interface, but VB developers are affected when viewing classes with
the VB object browser or dealing with outbound interfaces.
Access to default interfaces in VB
The VB environment hides the name of the default interface of a class, although its members are still accessible. VB
developers do not generally need to access the Iunknown interface; therefore, most ArcObjects classes define
IUnknown as the default interface. If you are creating an interface for use in VB, you may want to follow this
convention.
Default interfaces of components created in VB
When you create a COM class in VB, the VB compiler automatically generates a default interface for your class. This
interface contains all the public members you defined on your class and is named after the class with a prefix of an
underscore, for example _MyClass. You may want to provide access to your component from other environments, or to
gain more control over its definition for use within VB. If so, you might consider defining your interface in IDL, instead
of directly in VB. This gives you much more control over interface names and attributes and also over the types and
attributes of method parameters.
Default outbound interfaces of components created in VB
If you defined any events on your class, these are added to another automatically generated interface, this time
named after the class and prefixed with two underscores, for example __MyClass. As noted previously, you cannot
alter the outbound interface definitions due to the way VBs event model is implemented.
Classes with IDispatch as the default interface
A few ArcObjects classes specify the IDispatch interface as default; for example, the default interface of the
Application object for ArcMap is IApplication. The reasons for this and why you may want to have your classes
implement IDispatch are discussed in the section IUnknown, IDispatch, and Dual Interfaces below.
Optional interfaces
Throughout the ArcObjects object model diagrams, you will find interfaces marked as optional. Interfaces are marked
as optional on abstract classes for which some subclasses implement an interface and some do not. This is a
diagramming convention and does not affect the implementation of an interface.
Instance interfaces
The term instance interface describes an interface that is available on some instances of a particular class and not on
other instances of the same class. This concept does not break the rules of COM, as any particular instance of a class
must either always allow a QI or never allow a QI to the instance interface. This technique can simplify an object
model somewhat, making the components simpler to use as fewer subtypes are required.
Instance interfaces are found in particular throughout the Geocoding and Raster object models.
If an interface is marked as instance, you must always be careful to check the result of a QI before attempting to use
the interface.
Early binding, late binding, and IDispatch
If you intend to author a component that can be accessed from scripting languages, such as VB Script, JavaScript, or
another similar environment, you will find this section useful.
So far, this chapter has mostly concentrated on standard COM classes, with interfaces that inherit from IUnknown.
These classes can be used from compiled languages, such as VB and VC++, which bind function calls at compile
timeknown as early binding.
The function calls available on an interface are laid out in memory in a virtual tableit is these functions that the
compiler bind method calls. For this reason, early binding is sometimes also known as v-table binding.
However, not all environments are compiled this way. Scripting languages, such as VBScript and JavaScript, are
interpreted at run time, and therefore, require to bind method calls at run timeknown as late binding. The IDispatch
interface is designed to allow late-bound function calls, as the GetIDsOfNames and Invoke members allow function call
identification at run time. For this reason, components for use in scripting environments implement the IDispatch
interface.
A third type of binding is able to identify the IDs of methods at compile time using the IDispatch interface. Function
calls are bound to these IDs at compile time, so only Invoke must be called at run time. This type of binding is known
as dispID binding, and is also considered a type of early binding.
The advantages and disadvantages of the different types of binding are summarized below, indicating reasons why you
may want to avoid or choose particular implementations when defining new interfaces for your component.

Early binding using the v-table creates the fastest function calls; late binding is the slowest.

20

Early binding is only supported by environments that support v-table access.

Scripting clients generally can only access objects by late binding.

VB can access objects by both late binding (if variables are declared as type Object) and early binding (if declared
as the specific class type). DispID binding is also supported if a variable is declared specifically, but the object does
not support IUnknown and, therefore, has no v-table.

Most ArcObjects interfaces are custom, inheriting from IUnknown, and cannot be accessed from scripting clients. You
should be familiar with the concepts of early binding, v-table binding, and late binding from previous COM experience.
This topic is too complex to cover in detail heresee Introduction to COM for introductory information; if you need
further information, you should review the books about COM listed in the bibliography.
Dual interfaces
There is a way to delegate the early binding versus late binding decision to your client, and that is to use dual
interfaces on your class. As dual interfaces provide access to both the methods of IUnknown and IDispatch, it is
possible for VC++ clients to access the class using early binding and for script languages to access the class via late
binding.
The VB compiler automatically creates dual interfaces on VB classes, and VB classes are, in any case, restricted to
these variant data types.
One drawback to the dual interface model applies only to VC++ developers. The data types that can be used in a dual
or IDispatch interface are limited to the basic data types that can be wrapped as variantsthe full list of the data
types can be found in the OAIDL.idl header file in Visual Studio. Mainly, this excludes complex C++ structures. Dual
interface classes may be slightly larger in size, but the size increase is generally so small that it makes little difference.
One advantage for VC++ programmers using ATL is that the majority of the work to implement a dual interface is
done by the ATL wizards, meaning that little extra effort is required when compared to a custom interface.

Coding Interface Members


Some members will have certain programming issues associated with them, which developers should be aware of.
Client-side storage methods
In client-side storage methods, the client to the component allocates the memory required for the result of the method
before the method is invoked. The reason for client-side storage is performance. Where it is anticipated that a
particular method may be called in a tight loop, the objects for the method call's parameters need only be created
once outside the method, then populated inside each method call, which is faster than creating a new object inside the
method each time.
[Visual Basic 6.0]

Dim pEnvelope As IEnvelope


Set pEnvelope = New Envelope
For i = 0 To 10000
pPolygon.QueryEnvelope pEnvelope
' Do something with the envelope
Next i
Client-side storage methods are named beginning with Query, for example ISymbol::QueryBoundary and
IGeometry::QueryEnvelope, whereas methods beginning with Get generally instantiate the object for you.
Implementing a client-side storage method in VC++
Implementing a Query method is straightforward in VC++, as the environment is more suited to passing pointers
between methods in this way. In general, the same issues apply as just discussed for VB. Since pointers are passed by
value in VC++, changing the pointer to point to new memory will have no effect. Instead, use methods that work on
the existing object.
[Visual C++]

STDMETHODIMP CMyElement::QueryBounds(IDisplay *Display, IEnvelope *Bounds)


{
// Return error if object does not already exist
if (!Bounds) return E_POINTER;
// use cached coordinates
Bounds->PutWKSCoords(&m_envelope);
return S_OK;
}
As you are relying on the caller to pass a valid object, you should first check to see if the incoming reference is valid,
raising an error if you have received a null pointer.
[Visual C++]

STDMETHODIMP CMyElement::QueryBounds(IDisplay* pDisplay, IEnvelope* pBounds)


{
if (!pDisplay || !pBounds)
return E_POINTER;
...

21

Implementing a client-side storage method in VB


If you're developing in VB, you must be more careful with your object references when implementing a Query method.
For example, imagine you needed to create a custom graphic element. You will need to implement the IElement
interface. This contains the QueryBounds client-side storage method, defined in IDL as shown.
[in] IEnvelope* bounds
In VB, this appears as the following function.
[Visual Basic 6.0]

Private Sub IElement_QueryBounds(ByVal Display As esriDisplay.IDisplay, _


ByVal Bounds As esriGeometry.IEnvelope)
Notice that the Bounds parameter is passed by value. You might expect any changes made to this parameter to only
be valid within the context of the procedure, therefore, the variable's actual value remains unchanged once the
procedure exits. So how can you change the value of the object so that the caller can see the changes?
Consider what it means to pass something by value to a procedure.
The value of the parameter passed ByVal is copied, and the procedure receives the address of this copy to work with.
After the procedure exits, the temporary copy is discarded by the Visual Basic Virtual Machine (VBVM), as you would
expect.

However, if the parameter is an interface pointer to an object, this may have an unexpected effect. The value of the
interface pointer is copied and passed to the procedure ByVal, not the actual value of the underlying object. The new
temporary interface pointer in the called function references the same block of memory as the original interface
pointer, thus both the caller and procedure have references to the same underlying object in memory. Within the
procedure, using this temporary interface pointer to call methods and write data will change the data of the underlying
object, as long as you do not change the value of the pointer. When the procedure exits, the temporary pointer is
discarded. When control returns to the calling procedure, the changes to the underlying object can be seen.

However, if you change the value of the interface pointer (the object variable Bounds), while within the procedure, to
reference another object, then call methods and properties, you will be changing the data of the newly referenced
object.
[Visual Basic 6.0]

Private Sub IElement_QueryBounds(ByVal Display As esriDisplay.IDisplay, _


ByVal Bounds As esriGeometry.IEnvelope)
Set Bounds = pOtherGeometry.Envelope ' Only changes temporary pointer.

22

The data of the original underlying object is in this case left unchanged. Again, once the procedure exits, the
temporary pointer is discarded, and the calling procedure will still reference the original underlying object.
Begin coding your Query method in VB by defining a constant to represent the standard COM error indicating an
invalid pointer.
[Visual Basic 6.0]

Const E_POINTER = &H80004003


Now in your client-side storage method, check the incoming parametersraise the appropriate error if necessary.
[Visual Basic 6.0]

Private Sub IElement_QueryBounds(ByVal Display As esriDisplay.IDisplay, _


ByVal Bounds As esriGeometry.IEnvelope)
If Bounds Is Nothing Then
Err.Raise E_POINTER, Me.Name, "Invalid Pointer"
Exit Sub
End If
...
The incoming Envelope may contain information already. You can clear this using the SetEmpty method.
[Visual Basic 6.0]

Bounds.SetEmpty
Now you are ready to set the properties of the Envelope object. Avoid referencing the object again, using instead
methods that work on the existing object, for example, other Query methods.
[Visual Basic 6.0]

Dim pOutlineGeom As IGeometry


Set pOutlineGeom = New Polygon
IElement_QueryOutline m_pCachedDisplay, pOutlineGeom

' Use pOutlineGeom to set


'

properties of Bounds

pOutlineGeom.QueryEnvelope Bounds
Note, however, that the code above needs to instantiate a new polygon objectwhich is precisely the situation the
Query methods are designed to avoid. You should ideally cache such an object at a class level, reusing it to improve
performance.
Using AppRef
Custom classes, such as commands, tools, and extensions, gain a reference to the application they are instantiated in
by receiving a reference from an interface member. For example, the ICommand::OnCreate receives a reference to
the current Application as a parameter.
In many cases, such a convenient reference may not be available; for example, a custom Element has no such
reference passed to it. If you are creating a custom class that requires access to the rest of the Application to operate
correctly but does not receive such a reference, you can consider the following two options.
You could add a public method to your custom class, which takes a parameter referencing the Application (or other
appropriate class). This method would need to be called whenever the class is created in code. If your custom class
will only be created in code, and you can specify this requirement, this solution may be suitable.
In some circumstances, your component may be instantiated by code beyond your control; for example, a custom DDE
Handler is created by the ArcMap application itself by registering to a component category. In this case, it is not
possible to specify that the client must set a reference to the application after instantiating an object. In this case, it
may be possible to use the AppRef object, which can be instantiated within an ArcGIS application to get a reference to
the application object in that process.
However, in some circumstances, your component may be instantiated outside an ArcGIS application process, for
example, in an application which uses the MapControl or PageLayoutControl. In this case, attempting to instantiate
AppRef may cause an error, as no ArcGIS application object is running.
AppRef is used to get a reference to the current document. As sometimes a component may be
instantiated outside an ArcGIS application process, the component needs to account for this without
causing errors.
You should always, therefore, be careful when attempting to instantiate AppRef. The key issue is that your component
should always degrade behavior gracefully when using AppRef unsuccessfully. Always ensure that you have an active
error handler around code, which attempts to instantiate AppRef. Also, always make sure your code does not assume
the presence of the application or associated objects, but checks the references before use each time.

Creating Property Pages


A property page is often provided by the ArcGIS framework to allow user interaction with an object or set of objects.
Property pages can be found throughout the entire framework, although they are not often found on object diagrams.
Many examples described in this book include a property page implementation to allow the user to view and change
properties of the custom object.
Property pages allow users to interact with objects by changing the values of their properties without

23

writing code. A property page for a custom object also allows you to link online help files to a
particular object and could even be used to brand the object as your third party object.
A property page is not always essential for every custom object, even if other similar objects all have property pages.
For example, every ArcObjects symbol has an accompanying property page, but a custom symbol can be created
without a property page. In this case, the symbol can be used programmatically as required and will function as
expected. The lack of a property page will, however, limit user interaction with the symbol and also highlight the
symbol as a nonstandard object to the user.
This section describes the generic process of creating a property page to work within the ArcGIS framework. It
identifies the interfaces you must implement and describes how to code the members of these interfaces.

Property pages and property sheets


Before creating your own property page, you should know a little about the type of property pages used in ArcObjects,
as there are different techniques used by different development environments to create property pages.
Property pages should be a familiar concept, as they are found throughout many applications,
development environments, and technologies. To develop a property page for the ArcGIS framework
requires a certain set of standard interfaces to be implemented. This may be different from
techniques you have used before.
ArcObjects uses a standard COM design, whereby one or many property pages are contained by a property sheet. The
property sheet is a dialog box which relates to a certain object or set of objects. Each property page on the property
sheet contains controls to view and change the values of a set of related properties or to execute related methods on
the object or objects.

The Element Properties dialog box, shown here for a FillShapeElement, is a property sheet containing
many property pages, each providing access to a related set of properties of a FillShapeElement.
The range of property pages displayed in a particular property sheet is generally determined dynamically, using a
combination of mechanisms. Sometimes a property sheet contains a list of the class identifiers (CLSIDs) for all the
property pages it needs to display at runtime. This list of pages can be built dynamically at runtime by reading a
component category. In the previous example, the property sheet for a FillShapeElement coclass checks which
property pages to display by reading the 'ESRI Element Property Pages' component category.

Many property sheets determine their member property pages at runtime by using component
categories.

24

For example, the possible property pages for elements are found in the ESRI Element Property Pages
component category.
The same element property sheet is used for all the element coclasses and is, therefore, context sensitiveif you take
a look at the ESRI Element Property Pages category (use the Component Browser utility), you will see a number of
property pages displayed according to contextthat is, depending on the type of element selected.
So how does the element property sheet decide which particular property pages apply to the type of element selected?
The answer lies in the property pages themselves. The property sheet asks each property page whether or not it
applies to a particular object and only displays the pages that do apply. More information on how this mechanism
works can be found later in this section when the property page interfaces are discussed in more detail.
This model of property sheets and pages is applicable to many customization tasks. In many cases, it is likely that a
property sheet already exists for the kind of class you are creating, and you simply need to create a property page to
be displayed in this property sheet by ensuring your property page is registered with the appropriate component
category and applies to the appropriate kind of object. The sections 'Implementing a property page in Visual Basic' and
'Implementing a property page in Visual C++' describe how to achieve this kind of customization.
Often when creating custom objects, you can create a custom property page to be displayed in an
existing property sheet.
You can also instantiate a new property sheet and add any property pages you require to it. See the 'Displaying a
Property Sheet' section later in this chapter.
You can also create an entire property sheet, which has one or many property pages and can itself
be extended.
Examples of property pages can be seen, among others, in the LogoMarkerSymbol, VertexLineSymbol,
InfoTextElement, and TimestamperClassExtension examples in this book.

Embedded property pages


In the model thus far, many property pages all apply to a given coclass. For example, the property sheet for an
instance of the LineElement coclass displays both the Symbol and the Size and Position property pages, as both apply
to the LineElement coclass.
In some cases, the display of property pages is more complex. A property sheet may display one of a number of
property pages, which are mutually exclusive and depend on the underlying coclass type.
For example, the Color Browser property sheet (see below) displays a combo box from which you can select different
color models. Each color model is represented in ArcObjects by a different color coclass; CMYKColor, GrayColor,
HLSColor, HSVColor, and RGBColor. Each coclass has an applicable property page, which is displayed when the
appropriate color model is selected in the Color Browser property sheet.
When you select a different color model in the Color Browser, a different embedded property page is displayed, and
the Color Browser creates a new coclass of the selected type. The properties of the new color object are set to the
nearest approximation of the last selected color. When you click OK in the Color Browser, this new color object is
applied to the object being edited.

Embedded property pages are used to handle such situationsthese are property pages that are designed to be
contained inside other property pages or property sheets. Creating an embedded property sheet requires little more
coding than a standard property sheetsee the following sections for more information.
Generally, embedded property pages for use in a particular page or sheet are registered to a particular component
category. For example, the Color Browser displays embedded property pages found in the ESRI Color Property Pages
category. All property pages in such a category are considered mutually exclusive.
The Color Browser dialog box is a property sheet that displays a number of embedded property
pages. Selecting a different color model in the top combo box displays one of a number of embedded
property pages. When the user selects a new color model and the new page is shown, the visual
characteristics of the color from the previous page are preserved.
In most cases, certain properties from the object being edited by one property page can be transferred to the object
being edited by the new property pagefor example the Color Browser sets an approximation of the last selected color
to the newly selected property page.

25

Another example of an embedded property page is found on the Symbology property page of a layer. In this case, the
embedded property pages are displayed within another property page. A different embedded property page is
displayed depending on the type of renderer selected in the containing property page.

Property page interfaces


There are five interfaces that you should be familiar with when creating a property page, all of which are defined in
the Framework type library:
IPropertyPage
This is a standard interface defined by Microsoft as part of its COM implementation in Windows. It is implemented by
all the property pages in ArcObjects, providing functionality for both standard and embedded property pages. This
interface cannot be implemented in Visual Basic, as it contains several data types not supported by Visual Basic, for
example, unsigned long integers. (See IComPropertyPage below for the Visual Basic alternative to this interface.)
IPropertyPageContext
This ArcObjects interface provides additional functionality required by embedded property pages in ArcObjects and,
notably, provides the method Applies.
IComPropertyPage
This ArcObjects interface was designed for use specifically by Visual Basic developers. It includes similar functionality
to that found on the IPropertyPage interface, although you will notice that the members of these two interfaces are not
identical. IComPropertyPage also provides functions similar to some found on the IPropertyPageContext interface,
although it does not provide all the functionality required by an embedded property page.
IComPropertyPage2
This additional ArcObjects interface provides extra functionality to give the property page control over the ability of the
user to cancel the property sheet. You can implement this interface in VB, since it inherits directly from IUnknown and
replicates the members of IComPropertyPage.
Implementing this interface is optional, depending on whether or not this functionality is required. If you choose to
implement it, you must also ensure you implement IComPropertyPage. As most members of IComPropertyPage and
IComPropertyPage2 are common, you can delegate the work of these methods to secondary functions.
IComEmbeddedPropertyPage
This interface has similar members to some found on the IPropertyPageContext interface. See the previous section
Embedded property pages for more information.
More about property page interfaces
The interfaces you implement for a property page depend upon the development environment you are using.
Implementation of property pages in VB and VC++ is discussed in the following sections; however, it is worth noting
certain issues.
The interfaces you implement to create a property page vary according to the development environment and the type
of property page being created.
Throughout ArcObjects property sheets, the use of IPropertyPage and IPropertyPageContext is being superseded by
the use of IComPropertyPage and IComEmbeddedPropertyPage, as they are more flexible for third party developers.
However, you may come across property sheets that expect a property sheet to implement IPropertyPage and
IPropertyPageContext. In this case, you may want to implement both sets of interfaces. As the interfaces
IPropertyPage, IPropertyPageContext, IComPropertyPage and IComEmbeddedPropertyPage share many members in
common, you can write generic functions that you can call from all interfaces.
In addition to the interfaces noted here, there are a few specialist property page interfaces you may need to consider
implementing if you are creating certain types of property pages.
IRendererPropertyPage should be implemented if you are creating a property page for a custom renderer.
ISymbolPropertyPage should be implemented if you are creating a property page for a custom symbol. See Chapter 5
for examples of implementing both of these interfaces.
It is not recommended that you implement IDataConnectionPropertyPage, IDataConnectionPropertyPage2, or
IQueryPropertyPage, as these interfaces do not indicate a complete property page and are designed for internal use
only.
Property sheet passes a cloned object
A property sheet will clone its target object before passing it to a property page. This allows the property sheet to
discard the changes made by all the property pages to the target object if the user cancels the property sheet.

Implementing a property page in Visual Basic


As a Visual Basic developer, you may be accustomed to creating property pages by adding Property Page modules to
your project.
To create a property page for an existing ArcGIS property sheet, you will need to take a different approach. You will
create a coclass that implements the property page interfaces that ArcObjects expects to find. You will also create a
form to contain the user interface components to allow users to interact with the properties of your object.

26

A property page is implemented in Visual Basic by creating a form module, which contains the user
interface for the property page, and a separate class module implementing the required property
page interfaces. The two modules are then associated through your code.
Creating the property page
Follow these general steps to add a property page implementation to an existing project.
1.

Add a new form to your project and name it appropriately.

2.

Set the ScaleMode of the form to vbPixels.


You can now use the internal ScaleHeight and ScaleWidth properties of the form to return the Height and Width
of your property page, as required by the IComPropertyPage interface.

3.

Add controls to the form, as required, to allow users to edit the members of your custom class.

4.

Add a new class module to your project, name it appropriately, and set its Instancing property to Multiuse.

5.

Create a member variable in the class module to hold a reference to an instance of the form, for example:
[Visual Basic 6]

Private m_frmPage As frmMyPropertyPageForm


6.

In the class module, implement IComPropertyPage. Also, if required, implement IComPropertyPage2 and
IComEmbeddedPropertyPage. Add code to all the methods of these interfaces as described in the following
tables.
See the following pages for a summary of how to implement property page interfaces.
If the property sheet you intend to add your property page to does not check for IComPropertyPage, you also
need to implement IPropertyPage and optionally IPropertyPageContext.
Implement any specialist property page interfaces such as IRendererPropertyPage and ISymbolPropertyPage etc.
Use the variable declared in step 5 to create, show, hide and unload an instance of the property page form as
required. Do not forget to add code to translate the values of the controls on your form to the values of the
properties of the object you are editing.

7.

Complete the property page by adding code to the property page controls to change the properties of the object
passed in to IComPropertyPage::SetObjects.

8.

Compile the property page project and set binary compatibility.

9.

Register the property page class module with the appropriate component category or categories.
For more information on how to register a coclass to a component category, see the section 'Component
Categories', earlier in this chapter.
IComPropertyPage members and description

Activate

Called before the Show method when the user selects the property page, making it the current page
in the property sheet. Load the previously initialized Form and return the window handle of the page
site.

Applies

This method is called when the property sheet loads, before the dialog box is displayed. A reference
to an ISet object is passed in, which is a collection containing references to the objects to be edited
by the property page. The property page is responsible for checking to see if the objects in this set
can be edited by the page. Iterate through the set and, using the TypeOf keyword, check the objects.
If all the objects required for the page are present, then return true; otherwise, return false.

Apply

This method can be used to read the settings from the property page and apply them to the objects
you are manipulating with the page (those received in SetObjects), if those changes are not already
applied. This method is called when the user clicks either OK or Apply or changes the active property
page on the property sheet. See also the IsPageDirty property.

Cancel

Called when the cancel button is pressed on the property sheet.

Deactivate

Called when the property sheet exits; you should unload the form in this method.

Height

Returns the height of the property page, in pixels, from this read-only property, so the property
sheet will be sized correctly.

HelpContext If you have a helpfile, use this read-only property to return the appropriate help context ID number
ID
for the property page.
HelpFile

If you provide a helpfile for your component, return the filename of the helpfile from this read-only
property.

Hide

Called when a different page is selected, simply set the Visible property of the form to False.

IsPageDirty

The container of the property sheet checks this read-only property to see if the user has made any
changes to the property page that have not yet been applied to the object. You should return true if
changes have been made to the page; use a global variable to track changes made to the form since
the last call toApply. If you return false, the Apply method will not be called upon exit. Called after
Hide.

PageSite

A reference to an IComPropertyPageSite object is passed in to this method, which has a single


method called PageChanged. By calling this method the property page is able to inform the page site
that something has changed. Calling this method results in the Apply button becoming enabled.

27

Priority

A number of property pages can be displayed in a property sheet. The pages are ordered by the
read-write Priority property. The higher the priority, the sooner the page appears. Priority values are
usually between 0 and 100. If you want your page to display as the first page, using a value below
100 allows other pages to override your sheet, if necessary. Check the other property pages that
display in the same property sheet as your property page to see which Priority they have.

SetObjects

References to the objects to be edited are passed to the page by the SetObjects method in the
incoming ISet parameter. Save these objects as global variables. Later, when called to Apply, you
can apply the changes specified by the user to the objects passed in.

Show

Called after the Activate method when the user selects the property page. Simply set the Visible
property on your form to True.

Title

This property sets or returns the title of the property page, which is displayed on the page tab. It is
recommended that the form caption be used to hold the title.

Width

Return the width of the property page in pixels from this read-only property, for the property sheet to
be sized correctly.

IComPropertyPage2 members and description (see IComPropertyPage for details of other members)

QueryCancel

This method is called when the property page is the currently displayed page and the user clicks
the Cancel button before the property sheet is dismissed. Use this method to perform any checks or
changes before a user dismisses a property page. Return True to allow the dialog box to be
dismissed when the user clicks Cancel, or return False to prevent the Cancel operation.

IComEmbeddedPropertyPage members and description

CreateCompatibleObject

This method is called when the user changes the embedded property page that is
selected. Create a new object based on the properties of a template object, which is
passed in to this method. Note the object returned need not be the same type as the
template or even the objects specified in the SetObjects method, or it may be NULL.

QueryObject

The property page container will call this method, passing in a reference to an object that
applies to the property page, which provides the means for setting the changes from the
property page to the object being edited. Set the properties of that object based on the
values currently on the property page. Note that the type of object need not match that
passed to the SetObjects method.

Tips for property pages


Use a member variable in the form module to keep track of any changes made to the form, and use this to return the
IComPropertyPage::IsPageDirty value.
Check for invalid user input, such as alphabetic characters instead of numeric characters.
If you need to implement the IPropertyPage interface for compatibility with a particular property sheet, you will find
more information on the members of this interface in the following section 'Implementing a property page in Visual
C++'.
ArcObjects components separate user interface classes from nonuser interface classesa structure you may want to
copyallowing you to upgrade or update sections of your component independently.

Implementing a property page in VC++


In VC++, unlike in Visual Basic, a number of different approaches can be taken to implement a property page for the
ArcGIS framework.
The interfaces used by Visual Basic (IComPropertyPage, IComPropertyPage2 and IComEmbeddedPropertyPage) can all
be implemented in VC++, providing identical functionality.
Alternatively, the Active Template Library (ATL) property page template classes can be used, providing much of the
boilerplate property page code for you. Although this reduces the amount of code you need to write, it does have the
slight drawback that the QueryCancel functionality is not provided. This is an optional interface however, and if you
don't require this functionality in your property page, using the ATL approach can save you time.
Implementing the ArcObjects property page interfaces
If you choose to implement the ArcObjects IComPropertyPage interface, refer to the tables in the previous section
(Implementing a property page in Visual Basic) for details of the interface members. In this case, you may wish to use
the following tip to help you return the values of IComPropertyPage::Height and IComPropertyPage::Width.
1.

Define m_size as a member variable of type SIZE.

2.

Add the following code to your FinalConstruct.


[Visual C++]

HRSRC hRsrc = ::FindResource(_Module.m_hInst,


MAKEINTRESOURCE(IDD_SAMPLEPROPPAGE), RT_DIALOG);
if (hRsrc)

28

{
HGLOBAL hGlob = ::LoadResource(_Module.m_hInst, hRsrc);
DLGTEMPLATE* pDlgTempl = (DLGTEMPLATE*)::LockResource(hGlob);
if (pDlgTempl) _DialogSizeHelper::GetDialogSize(pDlgTempl, &m_size);
}
3.

For the get_Height and get_Width methods, set the output parameter to be m_size.cy and m_size.cx
respectively.

Using the ATL property page template classes


Should you choose to implement the property page using the ATL implementation for IPropertyPage, you must also
implement IPropertyPageContext. This provides the key member function Applies, among others.
The following steps take you through the initial setup of your property page ATL project.
1.

Create a new ATL project, and choose all the defaults (for example, a new DLL).

2.

Click the New ATL Object option from the Insert pulldown menu.

3.

In the ATL Object Wizard, click the Controls category and click the Property Page control.

4.

Click Next and, in the ATL Object Wizard Properties page, enter the short name for your property page. All the
other items on this page will be automatically completed for you.

5.

Now click the Attributes tab and choose your preferred settings.
It is recommended that you accept the default for all settings apart from Interface, which should generally be set
to Custom. For more information on custom interfaces and other details of implementing interfaces, see the
'Coding Interfaces' section of this chapter.

6.

Click the Strings tab and enter the string resources for your property page.

7.

Click OK and a blank form will be displayed. Add the controls you require for your property page.

Now inspect the generated classyou will find it inherits from the IPropertyPageImpl<> and CDialogImpl<> template
classes. The combination of these two classes provides the boilerplate code for the property page. The only method
that has been stubbed out to implement is the Apply method, with some commented out sample code. More
information about what code you need to put in the members on the IPropertyPage interface can be found in the
following table.
IPropertyPage overrides and description

SetObjects

Set the objects to be edited in the property page. The objects are passed in using a SafeArray of
IUknown pointers. The default implementation places these into the m_ppUnk[] array member
variable. It can be useful to override this method and set the values into your own member variables
using the interfaces you are interested in working with.

Show

The default implementation displays the property page. This method can be overridden to provide a
place to set the controls in the property page to the values held on the objects being edited.

Apply

This method is automatically stubbed out by ATL. It is the place where you read the settings in the
property page and update the objects via the interfaces passed in via the SetObjects member function.

Implementing IPropertyPageContext
The next step is to add the IPropertyPageContext interface to your class. Use the following steps to add the interface
to your class.
1.

In the Class view, click the popup menu over the class and click the Implement Interface button.

2.

In the Implement Interface dialog box, click the Add Typelib button, and the Browse Type Libraries dialog box
will be displayed.

3.

Search for the ESRI Framework Object Library, click it, then click OK. The Implement Interface dialog box will
now be populated with the esriFramework type library details.

4.

Search for the IPropertyPageContext interface, click it, then click OK. The IPropertyPageContext member
functions will be stubbed out in your header file, and COM_MAP will be updated to include the interface.

5.

As a result, the type library import will be added to your header file. Remove the import statement, as the type
library has already been imported into the StdAfx.h file.

More information about what code you need to put in the members on the IPropertyPageContext interface can be
found in the following table, which includes only those members that you will typically override in your property page
implementation.
IPropertyPageContext members and description

Applies

This method is called with an ISet containing the interfaces of the objects that are about to be
edited via a property sheet. Each page registered within a component category is responsible for
checking to see if the objects referenced are suitable for the page. This is performed by iterating
through the set and using the TypeOf keyword to check the objects. If all the objects required
for the page are present, then return true; otherwise, return false.

Cancel

Called when the Cancel button is clicked on the property sheet.

CreateCompatible Create a new object based on a template object passed in. Note: The object returned need not
Objects
be the same type as the template or the objects specified in the SetObjects method ) This

29

method is used to create objects suitable for being edited by the property page. A template
object passed in can be NULL if the page interacts with a single object. If its not NULL, it can be
used to identify the type of object required and allows properties to be copied from the
template.
GetHelpFile

Use this read-only property to return the filename of a helpfile if you have created one for your
page.

GetHelpId

Use this read-only property to return the help context ID if implementing help for your page.

Priority

A number of property pages can be displayed in a property sheet. By specifying the priority of
each property page, you are able to control the order of the pages. The higher the priority, the
sooner the page appears. The priority is a read-write property.

QueryObject

Called with an object, this method should set the values of the property page on that object.
(Note: The type of object need not match that passed into the SetObjects method.) This method
is used in embedded property pages when they are not interacting directly with the object, and
it provides the means for setting the changes.

Before attempting to implement a property page using ATL, it is recommended that you review the books in the ATL
section of the bibliography. Additional details for the IPropertyPage interface can be found in the Microsoft Developer
Network (MSDN) Library.
Registering to a component category
The next step to get your property page to display as required is to register the class in the appropriate component
category. For more information on how to register a coclass to a component category, see the section 'Component
Categories', earlier in this chapter.

Displaying A Property Sheet


You may decide that your application requires a customized property sheet. This may be because you have created a
new component for which no suitable property sheet exists in the ArcGIS framework, or you need to display a number
of property pages together and you want your custom property sheet itself to be extensible.
The ArcObjects ComPropertySheet coclass allows you to create a property sheet, and it gives the sheet an object to be
edited. The following VBA code demonstrates the basic steps using the IComPropertySheet interface. The principle is
exactly the same for VB or VC++ code.
[Visual Basic 6]

Dim pComPropSheet As esriFramework.IComPropertySheet


Set pComPropSheet = New esriFramework.ComPropertySheet
Dim pMarker As esriDisplay.IMarkerSymbol
Set pMarker = New esriDisplay.SimpleMarkerSymbol
Dim pMySet As esriSystem.ISet
Set pMySet = New esriSystem.Set
pMySet.Add pMarker
pMySet.Reset
Dim bOK As Boolean
bOK = pComPropSheet.EditProperties(pMySet, Application.hWnd)
Use the ComPropertySheet coclass to create your own property sheet dialog boxes.
Before displaying the property sheet, you can specify which pages you want to appear by using one of two possible
approaches:
1.

Specify a particular component category using the AddCategoryID method.


The component category should contain a list of property page coclasses. Before the property sheet is displayed,
each page in the category will be created and have its Applies method called. Every page in the category that
applies to the object will be displayed in the property sheet:
[Visual Basic 6]

Dim pUID As New esriSystem.UID


pUID.Value = "{818B37C0-F34E-11D2-BC8F-0080C7E04196}"
pComPropSheet.ClearCategoryIDs
pComPropSheet.AddCategoryID pUID
2.

Add individual property pages to the property sheet by creating the pages required and passing them to the
AddPage method:
[Visual Basic 6]

Dim pComPropPage As esriFramework.IComPropertyPage


Set pComPropPage = New esriDisplayUI.SimpleMarkerPropertyPage
pComPropSheet.AddPage pComPropPage

30

Specify which pages are to appear in the property sheet by using AddCategoryID, AddPage, or both.
By default, if no other pages are specified, the property sheet coclass will automatically check the ESRI Property Pages
category for pages that apply to the objects passed to the EditProperties method. All ArcObjects property pages are
registered with this category by default.
However, each property page has to be created, checked, and destroyed when checking this entire category. If you
use either or both of the approaches above to specify particular property pages, this can improve the display speed of
the property sheet.
The ComPropertySheet coclass has one outbound interface, IComPropertySheetEvents, with a single method called
OnApply. You may want to call this method to notify other parts of the application that the objects passed to the
property sheet have been edited.
Declare the event handler variable globally.
[Visual Basic 6]

Private WithEvents pComPropEvents As esriFramework.ComPropertySheet


Sink the event handler variable to the property sheet object when the object is created.
[Visual Basic 6]

Set pComPropEvents = pComPropSheet


Now you can call the OnApply method of the interface as required.
[Visual Basic 6]

Dim bOK As Boolean


bOK = (pComPropPage.EditProperties(pMySet, Application.hWnd))
If bOK Then m_pComPropEvents.OnApply
Your property sheet should now be ready to use. Be careful when opening the property sheet with the EditProperties
methodif the object passed in is not valid in some way, the property sheet will not be able to display. Check any
validity properties on the objects in the Set you passed to EditProperties before calling the method. You may also want
to check if any property pages are available by calling the CanEdit method, passing in this same Set.
See Also Design guidelines for property pages.

Design Guidelines for Property Pages and Dialog Boxes


Design guidelines for property pages and other dialog boxes
Consistent user interfaces not only help your components look professionalthey help your users navigate efficiently,
increasing the usability of your customizations.
Published guidelines are available for windows applicationsthe user interface guidelines in MSDN will help your dialog
boxes achieve the Windows 'look and feel'. In addition, the book The Windows Interface: An Application Design Guide
will give you complete information on the Microsoft approach to user interface design. See the bibliography for details
of these references.
In addition to those references above, you may want to review the standards below, which have been used throughout
ArcGIS. Based on the guidelines for Windows applications, these property page standards should help you ensure your
dialog boxes are visually and textually consistent and logical as well as have logical keyboard access.
The guidelines are also useful for other user interface items, such as dialog boxes and applications, not just property
sheets and pages.

Dialog Box Units


Both the Microsoft standards and the standards below use Dialog Box Units (DLUs) to define the size and location of
the controls in a dialog box.
A DLU is a device-independent unit, based on the current system font.
One horizontal dialog box unit is equal to one-fourth of the average character width for the current system font. One
vertical dialog box unit is equal to one-eighth of an average character height for the current system font. The default
Windows system font is 8-point MS Sans Serif, which gives a vertical dialog unit of approximately 22 twips.
Using DLUs allows dialog boxes to be sized correctly if the system font on the user's machine (where the dialog box is
displayed) is different from that on the development machine (where the dialog box was laid out).
Using DLUs
Laying out a dialog box in DLUs in VC++ is simpleDLUs are the units used by the VC++ form editor.
For VB developers, however, the situation is somewhat more complicated, as the VB form editor does not include a
ScaleMode of DLUs. You may want to use one of the following to estimate a DLU:

Use an approximate conversion valueapproximately 20 twips per pixel for the default system font.

Use the GetTextMetrics Windows API call, which returns value for the height and average width of a character in
the current font.

Create an invisible label on a form. Set the Text property to all the letters of the alphabet, and calculate the
height and average width of the characters from the size of the label.

31

Sizing and alignment


Dialog boxes and controls should generally be sized as follows:
Control

Height (DLUs)

Width (DLUs)

Small dialog box


Medium dialog box
Large dialog box

188
215
218

212
227
252

Command button

14

50

Option buttons, check boxes

10

as required

Text boxes

14

as required

Other text and labels

8 per line of text

as required

Dropdown combo and list boxes

10

as required

The default height for most single-line controls is 14 DLUs. Controls that contain text, such as edit boxes, option
buttons, check boxes, and labels, should be sized horizontally as required to display their contents and align correctly
with other controls in the dialog box.
Grouping and spacing
Controls within a dialog box should be at least 4 DLUs apart. Generally you should lay out controls in a dialog box
starting from the upper-left corner, using a 7 DLU margin between the edge of the dialog box and the contained
controls. Controls should generally be left-justified.
Related controls in a dialog box should be grouped together. You can group controls by using group box controls,
separators, or by altering spacing. If using group boxes, use a 4 DLU margin between the controls and the edge of the
group box. Increase the margin to 7 DLUs at the bottom and 14 DLUs at the top of the group box, to allow for the box
title. Left-align the controls with the box title text.

If the first control is a label (for example, accompanying an edit box), a smaller margin of 11 DLUs may appear more
consistent.
Make sure that if a particular command button applies only to a particular field that it is grouped with that field to
avoid confusion.
Separators should be used sparinglyonly where group boxes would be too overwhelmingbut there still is a need for
grouping related items. Often, increasing spacing around the related elements can be effective instead.
Aligning edit, list, and dropdown list boxes
These controls should be left-aligned (right-aligned for right-to-left languages), with accompanying text labels leftaligned, placed to the left or above the control. Note that dropdown list boxes should generally have their
accompanying text label above the control, although the label may appear to the left if it can be aligned with other
controls.

If an accompanying label is to the left of a box control, align the height of the label with the text displayed in the text
box.
Alignment and placement of command buttons
OK and Cancel buttons should be left-aligned if vertically stacked, and top-aligned if placed side-by-side.

32

Different types of dialog boxes require different placement of OK and Cancel command buttons. A property sheet will
always have OK, Cancel, and Apply placed at the bottom of the sheet below the property pages.
Standalone dialog boxes can have OK and Cancel aligned horizontally in the lower right corner or stacked on the top
right corner. In either case, the default button is typically the first button in the set, for example, the OK button, with
the Help button being last if it is present. OK and Cancel should always be next to each other.

Command buttons on a tab control within a dialog box should only apply to the controls on that particular tab;
whereas buttons outside the tab control apply to the entire dialog box.
All controls should have Help Context IDs. See the 'Implementing help for custom classes' section for more
information.
Capitalization
Text on command buttons, title bar text, icon labels, and tab titles should use conventional title capitalization
capitalize the first letter in each word. Articles (a, an) and prepositions (on, at, in, and so forth) are not capitalized
unless they occur at the beginning or end of the text. If a word is generally capitalized a certain way, use this method
(for example, ArcGIS, INFO database).

User-defined text should remain as the user specifies it, regardless of case.
Field labelsfor example, option button labels, check boxes, text boxes, group boxes, and page tabsshould use
sentence-style capitalization.
Using tab order for navigation in dialog boxes
You should always ensure you set an appropriate tab order for your formthis can easily be neglected during
development but is simple to do and makes it so much easier for users to navigate your dialog box quickly and
efficiently.
Set the order such that the user can move through the dialog box from left-to-right and top-to-bottom, which allows
the user to progress through grouped controls in a logical order. Command buttons, such as OK and Cancel, are
usually the last in the tab sequence. Label text should not generally take part in the tab sequence, but you should bear
in mind the rules for Access Key use described in the following section.

Because option buttons typically function as a group, you may want the tab order to move focus to the currently
selected option in that group, not between individual options. Arrow keys can instead be used to move between
options in a group. Check boxes, however, should have separate tab order settings.
If you're developing in VC++, you can set the tab order by selecting Tab Order from the menu or by pressing CTRL+D
and clicking the controls in the correct order.

33

VB Developers should set the tab order by setting the TabIndex property of each control individually, then running the
ESRI Align Controls With Tab Index VB add-in, which is part of the ArcObjects Developer Kit.
Using access keys to navigate dialog boxes
Access keys allow the user to set focus to any control in the dialog box by pressing and holding the ALT key in
combination with another alphanumeric key designated as the access key for that control.
In both VC++ and VB, access keys are designated as the character after the ampersand (&) in the control's text or
caption. The ampersand is not visible at run time, when instead the access key is shown underlined.
Note that for combinations of controls, such as a label and edit box control or a spin button and edit box, you can set
the access key typed in the label control to set focus to the accompanying control, which has no text. You do this by
ensuring the edit box control has a TabIndex value one greater than the label control and setting the TabStop property
of the label control to False.
Ensure you do not specify the same access key twice for a dialog box. This requires particular care for property pages,
which take part in a larger property sheetfor example, avoid using "a" or "A" as an access key because the property
sheet will use "A" for the Apply button.
Message boxes
Message boxes should be used in situations where the user must confirm an action or command and must
communicate the requirements effectively. Used inappropriately, they can interrupt the flow of workit is often better
to avoid using message boxes if possible.
An appropriate use would be to confirm a delete that cannot be recovered. An inappropriate use would be to inform a
user that an option is unavailabledisabling commands may be a more effective way to achieve this.
A message box should include four things:

The title bar should contain the source of the message.

The main window should contain descriptive text, asking a question or stating the situation.

Command buttons as required to gain user input to the question statedthe most frequently used or least
destructive option should be the default option.

A symbol that gives the message box context. See the following table.
Icon

VB MsgBox Icon constant /


Windows API MessageBox constant

Context

vbInformation
MB_ICONINFORMATION

Alerts the user to a condition or situation that requires a


decision before proceeding, especially potentially
destructive irreversible actions.

vbExclamation
MB_ICONEXCLAMATION

Used to show information about results of a command.


The only button should be OK.

vbCritical
MB_ICONSTOP

Informs the user of a serious problem that requires


correction or intervention before work can continue.

vbQuestion
MB_ICONQUESTION

Informs the user that a response to the statement is


needed before execution can continue. It may have one
or more buttons for the user to click.

Your descriptive text should be clear and concise, avoiding the overuse of technical jargon. Try not to exceed three
lines of text, providing only the necessary information, but enough to adequately describe the situation. If there is a
problem that needs to be solved, suggest a solution and alternatives. Use complete sentences.

Progress Indicators
The Cancel button can often be used to interrupt a task in process. Only use the Cancel button if your code then
ensures the application is returned to its previous state; otherwise, use the Stop button.
See also Property Pages

Component Categories
COM and the registry
The Windows system registry is used extensively to store information about COM systems. The HKEY_CLASSES_ROOT
hive of a machine's registry contains information about all the COM classes, interfaces, type libraries, applications, and
so on, registered on the system.
Each component is given a unique identifier (a GUID) that the COM runtime environment, called the Service Control
Manager (SCM), uses to identify the component during execution. The details of COM's use of the registry can be
found in the references included in the bibliography.

34

ArcObjects is a COM system, and therefore, the standard information about the classes, interfaces, and so on, that
comprise ArcObjects can be found in the registry of any machine that has ArcGIS installed.
Developing software using COM offers the ability to update and customize an application incrementallyit is COM's use
of the registry that helps achieve this.
Use of component categories
COM systems also often use another area of the registry, called component categories. Conceptually, a component
category is a convenient way to logically group together classes that provide a certain type of functionality. Generally,
all the classes in a particular category will support an agreed set of interfaces, although sometimes the classes simply
conform to a semantic description of functionality.
Any application can read the contents of a component category at run time to gather information about which classes
support certain functionality without that application needing to know precise class names in advance.
ArcObjects makes extensive use of component categories to improve the extensibility of ArcGIS. You can also use the
same component categories to help you extend ArcGIS.
Component categories support the extensibility of COM by allowing the developer of the client
application to create and work with classes that belong to a particular category.
The client application does not need to know in advance the exact names of the coclasses in the
category, as the coclasses in the category are found at run time.
About CATIDs
All the component categories on a machine can be found by opening the appropriate registry key.
HKEY_CLASSES_ROOT\Component Categories
Each subkey identifies a category; each subkey name is a unique identifier or GUID, which is referred to as a CATID.
Each CATID key contains a descriptive name for the category as a string data value. The ESRI Workspace Factory
component category is shown below, viewed using the Windows Regedit utility.

A CATID is a unique identifier for a particular component category.


You can see the CATID in the left pane, and the descriptive name in the right (the 409 value indicates the U.S. English
locale ID, which is the locale of all the ESRI descriptive strings).
A CATID key does not contain subkeys of classes belonging to that category, as you may have expected. Instead,
component category information for a specific class is stored along with the other class information for that class,
under its Class ID (CLSID) registry entry, in a subkey called Implemented Categories. Each Implemented Categories
key contains one or many subkeys, which contain the CATIDs of the categories the class takes part in.
A class indicates that it takes part in a component category by including the relevant CATID in the
Implemented Categories subkey of the class's registry entry.
For example, the ArcInfoWorkspaceFactory coclass registry key is shown below. You can see that the Implemented
Categories subkey contains three CATID subkeys. The first CATID is the ESRI Workspace Factory category; the other
CATIDs indicate that the ArcInfoWorkspaceFactory is also part of the Automation Objects and Arkansas Objects
categories.

All the component categories defined by ArcGIS can also be found in a Visual C++ header file, and a Visual Basic class
module, which are part of the Developer Kit installed on your machine. These files can be found in the \Include\CatIDs
subfolder of your installation.

35

The use of ESRI component categories in ArcGIS


Component categories are used in two different ways by ArcGIS: at application startup and as required throughout the
framework.

Categories read at application startup


Some component categories are always read when an ArcGIS application starts up. Coclasses registered to these
categories are instantiated at this point, and the instance is kept in memory as long as the application runs.
ArcGIS applications read some component categories at application startup.
The ESRI Mx Extensions category is one example of a component category used in this way. By registering to
this category, classes indicate that they implement IExtension and can be run as an extension to the ArcMap
application. An instance of each of these classes is created when the ArcMap application starts up. If the
extension class is removed from the category, the extension class will not be instantiated, even if a map
document with a reference to the extension is opened.

Categories read during execution


Some categories are read as required during the execution of an ArcGIS application. Objects registered to these
categories are instantiated and destroyed as required by the application.
Some categories are read during execution, depending on the actions of the user.
The ESRI Mx Commands category is an example of a component category used in this way. ArcMap reads this
category before displaying the Customize dialog box to the user. Once the user selects a command and adds it
to a toolbar, the map document will maintain a reference to that command class, even if the class is
subsequently removed from the ESRI Mx Commands category.

Using ESRI component categories in custom components


When creating custom components for ArcGIS, you will often need to register your classes to component to a
component category to work with your class in the same way as existing ArcObjects classes.
You will need to work out which category or categories to register your class to and how best to go about the task of
adding this information to the registry.
Bear in mind that a class may often be registered to more than one component category (to perform a task at
different points of execution) or to no component categories at all (if a class is only referenced directly in code, the
component category is superfluous). However, registering to appropriate categories is a useful technique, providing
the flexibility for your own code to find classes at run time, and therefore to be updated more easily in the future. You
could also create your own component category, if appropriate, to leverage this functionality further.

Methods of registering to a component category


The process of registering a component to a category depends largely on two factors.

Each development environment provides a different type of support for component category registrationsome
environments provide a number of built-in ways to write to the registry.

You may want to use a simple, on-demand method of registration during your development and testing cycles
and a more automated method when deploying that component.

As well as the standard methods of registration that may be available in your development environment, there are
also methods of registration independent of your environment.
ESRI provides a number of ways to register classes to a component category, which may be particularly useful to VB
programmers, as the VB environment provides little support for this.
You can register classes to a component category using one of several methods. The methods you
can use may depend on your development environment.
Adding commands using the Customize dialog box
Using the Customize dialog box, you can register Commands and command bars to the appropriate category.
You can add classes that implement ICommand to a commands component categories using the Add
From File button in the Customize dialog box.
Start by clicking the Tools menu and choosing Customize. Then click Add from file, and browse to your DLL. All classes
in that DLL which implement ICommand are then added to the list of commands shown in the dialog boxthe
Categories list refers to the ICommand::Category property of the command classes.
These classes will also be registered to the appropriate commands component category; if you are using ArcMap, the
classes will be registered to ESRI Mx Commands; for ArcCatalog the category will be ESRI Gx Commands; and for
ArcScene the category will be ESRI Sx Commands.
Below you can see the Create Layer Files sample has been registered in ArcMap using the Customize dialog box. The
left- and right-hand panes together list all components registered to ESRI Mx Commands.

36

This dialog box is most useful for registering sample commands and when testing.
You cannot remove a component from a category by using the Customize dialog box.
This functionality is ideal for use in an ad hoc testing environment for registering new commands and tools. It is also
perfect for registering ESRI sample commands and tools.
However, this dialog box is limited in use, since it can only be used to register commands to the application commands
component categories. Also, it is not possible to remove a class from a component category using this dialog box.
To remove a component registered in this way, you must first make sure any references to the class are removed from
your documents by removing from toolbars, menus, and so on. The component category registry entry should then be
removed manually, for example, by using one of the methods described in the following sections. Note that if you
unregister your entire DLL using COMUnregisterServer, this process will also remove the component category
registration for the class.
Using Component Category Manager
Distributed with ArcGIS is a utility called the Component Category Manager, that allows you to add and remove classes
from any component category.

The Component Category Manager is a more flexible way of adding classes to, and removing classes
from, component categories.
When you expand a component category in the tree view, Component Category Manager searches the registry and
displays a list of all classes that are registered to that category. Use the Find button to find a category name that
contains the search string, and reduce the amount of time you spend scrolling through the tree view.
Select a category, and then click the Add Object button, then in the Add Objects dialog box, browse to your DLL, and
click Open. A checklist of all the classes in the server is then displayed, allowing you to register only the classes you
want to the category.

37

The Remove Object button will remove the selected class from the categoryit will not remove any other
Implemented Categories from the class or unregister the class on the system.
This utility is ideal for registering noncommand items in an ad hoc testing environment, but it is unsuitable for
deployment situations, as user interface interaction is always required.
Using the VB Compile and Register add-in
The VB Compile and Register add-in can be used to register any VB class to any component category at compile time;
it can also be used to create registry scripts for later use.
The Visual Basic Compile and Register add-in allows you to register components to categories at
compile time. It also creates registry scripts, which can be used to register components on other
machines.
All the classes in your project are listed in the left-hand paneselect a class, and then select the categories to register
the class to.

Commonly used categories are shown in the right-hand pane by default, but you can add to this list by choosing
Components, then Select Component Categories.
Clicking the Compile button will save all the files in the project and also create a registry script containing the required
registry entries. The project binary files are then compiled, and the component category information is added to the
registry.
To unregister a class, select the class in the left-hand pane, uncheck the appropriate component category, and compile
the project again.
If you are developing in VB, you may find the Compile and Register add-in an efficient way of registering your
components at compile time. This method is ideal for a testing environment, where a component may need to be
registered repeatedly. It also automatically generates a registry scripts, which can be used later when you want to
deploy your component.
VB Debug Helper
When you run a VB application in Debug mode (by clicking the Run button), the VB Debug Process removes all the
registry entries for the compiled component and replaces them with information pointing to the VB debug process
itself.
This can lead to unexpected behavior when you are debugging a component which is registered to a component
category, as the debugger does not replace component category information, and therefore client applications find
your components are no longer registered to the component category.
The Compile and Register add-in provides the option to create a debug helper executable for your project. This

38

executable registers the components in your project to the chosen component categories after the debug session has
started, thus the client (for example, ArcMap) will find your components successfully. Your debug session will be
unaffected.
To use the VB debug helper, open the Compile and Register dialog box and ensure the Support Visual Basic Debugger
option is checked. Also, check the appropriate ArcGIS application.

Once you have compiled your project with the add-in, open the Project Properties dialog box in VB, select the
Debugging tab, and set the Start Program to the EXE generated by the add-inthis will be called
EsriVBDebugHelper.exe and will be located in the project folder.
Using registry scripts
Components can also be registered by entering information directly to the registry. This is most commonly done by
running a registry script. A registry script is simply a plain text file with the extension .reg. Running the script (for
example, by double-clicking the file in Windows Explorer or by running from a batch file) will enter the contained
information to the registry.
The first line of the file specifies the intended version of the registry editor. After a blank line, the required registry
entries are listed, each separated by a blank line. A registry entry is specified by enclosing the required registry path in
square brackets. Comments are preceded by a semicolon.
For example, the Create Layer Files sample you saw previously could be registered to the ESRI Mx Commands
category by executing the following script.
; CoClass: CreateLayerfiles.clsCreateLayerfile
; CLSID: {AEFC673B-17D7-11D4-B77E-0080C71C4226}
; Component Category: ESRI Mx Commands
[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{AEFC673B-17D7-11D4-B77E-0080C71C4226}\Implemented
Categories\{B56A7C42-83D4-11D2-A2E9-080009B6F22B}]
(Note that the last two lines of this registry script should be one continuous line in the .reg file).
Registry scripts can be used to add information to the registry.
To see examples of registry scripts, you can use the VB Compile and Register add-in, or you can export existing
registry entries to a .reg file using the Regedit utility.
Registry scripts are useful if you are developing in VB and need to distribute your application, as the script can then be
run as part of an installation program on the target machines. If you are developing in VC++, you would generally
place registration code into the COMRegisterServer function (see Registering components built with VC++ and ATL
below).
Registry scripts may be useful when deploying a component to other machines.

Programming with the ComponentCategoryManager coclass


The ComponentCategoryManager class is provided by ESRI to help you register your components to component
categories programmatically.
ESRI provides this class as an alternative to the COM standard component category manager class, which has
interfaces that aren't VB friendly. In addition, the ESRI class optimizes the process of searching the registry for
component category information by caching category information.
Using IComponentCategoryManager, you can create a new component category, register classes to any component
category, and also remove classes from a category. The following VB code adds all the classes that implement
ICommand in the specified MyCommands.dll to the ESRI Mx Commands category.
[Visual Basic 6]

Dim pInterfaceUID As New UID, pCatUID As New UID


pInterfaceUID.Value = "{36B06538-4437-11D1-B970-080009EE4E51}" ` ICommand

39

pCatUID.Value = "{B56A7C42-83D4-11D2-A2E9-080009B6F22B}" ` ESRI Mx Commands


Dim pCategoryMan As IComponentCategoryManager
Set pCategoryMan = New ComponentCategoryManager
pCategoryMan.Setup sMyPath & "\MyCommands.dll", pInterfaceUID, pCatUID, True
ComponentCategoryManager may be useful to both VB and VC++ developers, for runtime component category
editing, or can be used to create a program to run as part of an install.
Registering components built with VC++ and ATL
If you have used the ATL COM Object Wizard to create your component, your project will include a registry script file
(.rgs) for each class. This registry script is executed when the server is registered or unregistered on a machine.
You can add code to register your class to a component category here, although it is generally easier and more
appropriate to register to a component category by using the ATL category map macros.
Visual C++ offers a number of ways to register components to a component category. You can use
the ATL category map macros, registry scripts, or alternatively, the ComponentCategoryManager
coclass, adding code to the DLLRegisterServer method.
Use these macros in the header file of your component. Whenever your server is registered (in the COMRegisterServer
and COMUnregisterServer functions), the components will be registered to the specified component categories. As an
example, this code registers a zoom-in class to the ESRI Mx Commands category.
[Visual C++]

BEGIN_CATEGORY_MAP(__uuidof(CATID_MxCommands))
IMPLEMENTED_CATEGORY(__uuidof(CATID_MxCommands))
END_CATEGORY_MAP()
You can obtain the CATIDs of the standard ESRI component categories from the ArcCATIDs.h header file provided as
part of the ArcGIS installation. Include this header file in your project:
#include "C:\Program Files\ArcGIS\include\CatIDs\ArcCATIDs.h"
Alternatively, you can add to the self-registration code of the server to register a class to a category. This can be done
either using the Microsoft Component Category Manager coclass or the ESRI ComponentCategoryManager coclass.

Implementing Cloning
Cloning is the process of copying an objectthe creation of a new instance of a class, representing information
equivalent to the original instance.
Creating a copy of a particular object is more complex than simply assigning a new variable. For example, the code
below simply creates two variables that point to the same object in memory.
[Visual Basic 6]

Dim pPointOne As esriGeometry.IPoint


Dim pPointTwo as esriGeometry.IPoint
Set pPointOne = New esriGeometry.Point
Set pPointTwo = pPointOne

Copying an object is more complex than simply assigning a new variable.


To actually copy the Point object, creating a new instance of a Point with comparable data to the first Point, use the
IClone interface.
[Visual Basic 6]

Dim pClone As esriSystem.IClone


Set pClone = pPointOne
Set pPointTwo = pClone.Clone

Cloning creates a new instance in memory.

40

The concepts reviewed in this section should be familiar to any VC++ developer; however, some VB developers may
find they do not normally consider these kinds of issues.
Cloning in ArcGIS
This technique is used extensively throughout ArcGIS by ArcObjects classes that implement the IClone interface.
Cloning is a technique used extensively throughout ArcGIS.
For example, before the Application passes an object to a property page, it clones the object. Only if the OK or Apply
button is pressed are the properties of the cloned object set into the original object. Another use of cloning in
ArcObjects is by methods or properties, which specifically return a copy of an object, for example, the
IFeature::ShapeCopy method.
You can also find other examples of how cloning is used by searching the samples included in the ArcGIS Developer
Help.
Terminology
Throughout this section, the original object will be referred to as the Clonerthis object performs the cloning
operation. The object resulting from the cloning process will be called the Clonee.

Copying members: Values and object References


The exact details of the clone operation are encapsulated in the class implementationthe class regulates which of its
members should be copied and also how they should be copied.
Each class that implements cloning decides how to clone itself.
Shallow and deep cloning
For a simple object whose members contain only value type information, the cloning process is relatively simple. A
new instance of the class is created, and the values of all the members of the clonee are set to equal the values of the
cloner. The clonee object is independent of the cloner.
For an object whose members contain object references, the cloning process becomes more complex. Should the
cloner copy only the object references to the clonee? This is sometimes known as a shallow clone. Or should new
instances of each of the referenced objects be created also and the clonee's members be set to reference these new
objects? This is referred to as a deep clone.

There are different levels of cloning, shallow and deep.


Both shallow and deep cloning are used by ArcObjects classes. An example of a deep clone, where referenced objects
are also cloned, is when a graphic element is cloned. Both the Geometry and Symbol of the graphic element are also
clonedthe Geometry and Symbol properties of the clonee are entirely separate from the original objects' Geometry
and Symbol.
In other cases it is logical to simply copy an object reference to the new object. Shallow cloning is used, for example,
on the geometries in a FeatureClass. Every geometry has an object reference indicating its coordinate system
(IGeometry::SpatialReference). Cloning the Geometry produces an object with a reference to the same underlying
SpatialReference object. In this case, only the object reference is copied, as it is logical for all geometries in a
FeatureClass to hold a reference to the same spatial reference object, as does the layer itself. The SpatialReference
can then be changed by a single method call.
There is no simple rule for deciding whether an object reference should be copied or the referenced object itself should
be cloned. This is decided on a case-by-case basis, and both techniques may be included in a single class.
For each of its private members, a class needs to make the appropriate choice between shallow or
deep cloning.
You must be particularly careful when cloning objects that hold object references. In many cases, a referenced object
may hold references to yet more objects, which in turn hold references to other objects, and so on.
Transient members
When coding a clone method, bear in mind that some members should not be directly copied at allwindow handles
(hWnd), device contexts (hDC), file handles, and Graphical Device Interface (GDI) resources, for example, contain
instance-specific information, which should not be duplicated directly to another object.
In most cases, it is inappropriate to clone an object that contains this type of instance-specific information. For

41

example, a Workspace and FeatureClass both have connection-specific information and are not clonable; an
OverviewWindow and a ToolControl both have a window handle and are not clonable. If a new instance is required, the
object is created from scratch.
Sometimes it is more appropriate for a class not to replicate a member in its clone at all.
If you need to implement IClone on such an object, ensure that any instance-specific information is populated from
scratch, instead of simply copying the instance-specific values.

Implementing IClone
If you implement cloning in your custom components, you will need to make some decisions about how you copy the
information contained in your classwhether shallow or deep cloning is most appropriate for each member and how to
implement this.
The sections below show you how to implement each of the IClone members in your custom class.
Two different approaches are discussed. The first approach is straightforward and can be implemented using similar
logic in either VB or VC++. The second approach can only be used in VC++, as it uses a class's own persistence
implementation to perform the clone.
Coding IClone in VB
In the Clone method, begin by creating a new instance of the class, which is the clonee. You can then call the
IClone::Assign, to copy the properties of the cloner to the clonee. Lastly, return a reference to the clonee from Clone.
[Visual Basic 6]

Private Function IClone_Clone() As esriSystem.IClone


Dim pNewObject As MyLibrary.MyClass, pClone As esriSystem.IClone
Set pNewObject = New MyLibrary.MyClass
Set pClone = pNewObject
pClone.Assign Me

' pNewObject represents the Clonee.


' Me represents the Cloner.

Set IClone_Clone = pClone


End Function
Clone should create a new instance of the class.
The Assign method receives a reference to a second instance of the class, srcthis is the clonee. First, check src to
see if it is pointing to a valid objectif not, raise the appropriate standard COM error.
[Visual Basic 6]

Const E_POINTER = &H80004003


Assign should receive a valid instance of the class.
Then copy the values of the members from src (the clonee) to the current instance of the class, Me (the cloner).
[Visual Basic 6]

Private Sub IClone_Assign(ByVal src As esriSystem.IClone)


If (src Is Nothing) Then
Err.Raise Const E_POINTER
Exit Sub
ElseIf TypeOf src Is MyLibrary.IMyInterface Then
Dim pSrcMyInterface As MyLibrary.IMyInterface
Set pSrcMyInterface = src
' m_MyMember is a class member storing value of MyMember property.
m_MyMember = pSrcMyInterface.MyMember
End If
End Sub

The cloner copies values from the clonee.


The Assign code above shows a shallow clone of the MyMember property. If MyMember is another object reference,

42

you may want to perform a deep cloneif the object itself supports IClone, this is straightforward.
[Visual Basic 6]

Dim pCloned As esriSystem.IClone


Set pCloned = pSrcInterface.MyMember
Set m_MyMember = pCloned.Clone
If the member object does not support IClone, you must create a new object and set its properties from the existing
MyMember property of the source object, scr.
Remember to think about whether it is more appropriate to copy just an object reference (for example, all the
geometries of a FeatureClass hold a reference to the same SpatialReference), clone the object reference, or leave the
member uncopied to be set by the client code as appropriate.
When coding the Assign method, you should consider the choice of shallow or deep cloning. Consider
that some member variables may not be suitable for cloning.
As an example, consider how a RandomColorRamp performs an Assign. The cloner RandomColorRamp will have the
same MinSaturation, MaxSaturation, MinValue, MaxValue, StartHue, EndHue, UseSeed, Seed, and Name as the clonee.
However, the Assign method does not copy the value of Size or call the CreateRamp method; this means the color
ramp has no array of Colors and cannot be used in a renderer at that point. After a call to Assign, the client must set
up the Colors array of the RandomColorRamp by setting its Size property and calling its CreateRamp method.
Another consideration when coding your Assign method should be the current state of both the cloner and clonee
objects. You may decide to clear any stateful information held by the cloner before assigning the properties from the
clonee. In this case, you may want to add an internal initialization function to set the values of the class to a known
initial state. This function could then also be called from your class initialization function.
You may want to clear or reinitialize any member variables before performing an Assign to ensure
the result is a faithful clone.
The IsEqual method should compare the cloner (Me) and the clonee (other) to see if all the members are equal in
valuereturn True if all the members are equal.
[Visual Basic 6]

Private Function IClone_IsEqual(ByVal other As esriSystem.IClone) As Boolean


IClone_IsEqual = True
If (src Is Nothing) Then
Err.Raise Const E_POINTER
Exit Sub
ElseIf TypeOf other Is MyLibrary.IMyInterface Then
Dim pSrcMyInterface As IMyInterface, pOtherMyInterface As IMyInterface
Set pSrcMyInterface = other
Set pOtherMyInterface = Me
IClone_IsEqual = IClone_IsEqual And _
(pOtherMyInterface.MyMember = pSrcMyInterface.MyMember)
...
End If
End Function
If a property holds an object reference that supports IClone, use IClone::IsEqual on the member object to evaluate if
it is equal to the member object of the passed-in reference, other. Don't forget to check all the members of all the
interfaces that are supported by the object.
IsEqual should determine if two different objects have values that can be considered equivalent.
You decide what your class considers to be Equal valuesyou may decide that two IColor members are equal if they
have the same RGB value, even though one is an RGBColor and one is a CMYKColor.
To implement IsIdentical, you should compare the interface pointers to see if the cloner (Me) and the clonee (other)
point to the same underlying object in memory. In VB, you can compare two object references (or interface pointers)
using the Is keyword.
[Visual Basic 6]

Private Function IClone_IsIdentical(ByVal other As esriSystem.IClone)_


As Boolean
IClone_IsIdentical = False
If (src Is Nothing) Then
Err.Raise Const E_POINTER
Exit Sub
ElseIf TypeOf other Is MyLibrary.IMyInterface Then
Dim pOtherMyInterface As MyLibrary.IMyInterface
Set pOtherMyInterface = other
IClone_IsIdentical = (pOtherMyInterface Is Me)
End If

43

End Function

IsIdentical should compare interface pointers to see if they reference the same underlying object.
Coding IClone in VC++
IClone can be implemented in VC++ with a similar approach to that just described for VB. The Clone method is shown
below.
[Visual C++]

STDMETHODIMP CMyClass::Clone(IClone **Clone)


{
if (!Clone) return E_POINTER;
*Clone = 0;
HRESULT hr;
CComObject<CMyClass>* pItem = 0;
hr = CComObject<CMyClass>::CreateInstance(&pItem);
if (FAILED(hr)) return hr;
pItem->AddRef();
IClonePtr ipClonee = pItem;
if (ipClonee == NULL) return E_FAIL;
pItem->Release();
IClonePtr ipCloner = this;
if (ipCloner == NULL) return E_FAIL;
hr = ipClonee->Assign(ipCloner);
if (FAILED(hr)) return hr;
*Clone = ipClonee.Detach();
return S_OK;
}
Note the use of the CComObject static member function CreateInstance, rather than the normal object creation via
COMmore information on private initialization can be found on page 148 of ATL Internals (see bibliography for more
details).
Implement the Assign, IsEqual, and IsIdentical methods by using the same principles as shown previously for VB.
Coding IClone methods in VC++ using an ObjectStream
If your VC++ class implements IPersist and IPersistStream, you can take advantage of the persistence functionality
when writing your clone methods. By temporarily saving your object to an ObjectStream, you can duplicate the object
by creating a new instance of your class and loading its properties from the temporary ObjectStream.
This technique is used internally, for example, when you cut and paste graphic elements in ArcMap. This technique
may result in a more efficient Clone method when working with a complex class with many object references.
This technique is not directly available to VB developers as the signature of IObjectStream::LoadObject is not usable in
VB.
The code below shows how you could create an implementation of the Clone method using an ObjectStream. The
enumerations used in this code (and IsEqual which follows) are part of the COM platform SDK.
Begin by checking the incoming pointer.
[Visual C++]

if (!Clone) return E_POINTER;


*Clone = 0;
HRESULT hr;
Next, create a memory stream using the COM platform SDK function, CreateStreamOnHGlobal. Also, create an
ObjectStream, then aggregate the simple stream into the ObjectStream.
[Visual C++]

IStreamPtr ipStreamPtr;

44

::CreateStreamOnHGlobal(NULL, TRUE, &ipStreamPtr);


if (ipStreamPtr == NULL) return E_FAIL;
IObjectStreamPtr ipObjectStream;
hr = ipObjectStream.CreateInstance(CLSID_ObjectStream);
if (FAILED(hr)) return hr;
hr = ipObjectStream->putref_Stream(ipStreamPtr);
if (FAILED(hr)) return hr;
After checking for the presence of IPersistStream, persist the object to the memory stream.
[Visual C++]

IUnknownPtr ipUnknown = GetUnknown();


IPersistStreamPtr ipPersistStream = ipUnknown;
if (ipPersistStream == NULL) return E_FAIL;
hr = ipObjectStream->SaveObject(ipUnknown);
if (FAILED(hr)) return hr;
When you have finished persisting to the stream, reset it to the beginning.
[Visual C++]

ULARGE_INTEGER newPosition;
LARGE_INTEGER moveTo;
moveTo.QuadPart = 0;
ipObjectStream->Seek(moveTo, STREAM_SEEK_SET, &newPosition);
Then clone the object from the stream in memory.
[Visual C++]

IUnknownPtr ipCloneeUnk;
hr = ipObjectStream->LoadObject((GUID*)&IID_IUnknown, NULL, &ipCloneeUnk);
if (FAILED(hr)) return hr;
IClonePtr ipClonee = ipCloneeUnk;
if (ipClonee == NULL) return E_FAIL;
*Clone = ipClonee.Detach();
return S_OK;
An advantage of this approach to cloning is that the solution is generic. You can use the same code to implement
cloning on many classes, as long as they already implement IPersistStream.
Using this technique, a deep clone will be performed, as each object reference will be called on to persist itself to the
new stream. Check that this is a suitable operation for your class, particularly if your class holds references to objects
referenced elsewhere in the application or MxDocument. For example, a custom GraphicElement implements
IGraphicElement, which holds a reference to a SpatialReference objectthis object is a property of the Map in which
the GraphicElement resides. If the GraphicElement is cloned, the new object should also hold a reference to this same
SpatialReference object, NOT a reference to a separate but equal SpatialReference.
If you do use this technique for cloning such classes, you should reset each of the members you prefer to be shallow
cloned after the call to LoadObject.
The Assign method can be implemented similarly to Clonesave the supplied source object to a memory stream and
use IObjectStream::ReplaceObject to assign to the object in question.
The IsEqual method can be implemented by saving each object to a separate stream, then performing a byte-by-byte
comparison. One way to perform this operation is described step-by-step below.
The IsEqual method receives two parameters, a pointer to a second instance of a clonable class (pOther) and a
boolean (pbEqual) to return the outcome of the IsEqual operation. Initialize pbEqual to false, and check that pOther
points to a valid instance.
[Visual C++]

*pbEqual = VARIANT_FALSE;
if (!pOther)
return S_OK;
Next, create a MemoryBlobStream and persist the current instance of the class to this stream.
[Visual C++]

IStreamPtr ipStreamPtr1;
::CreateStreamOnHGlobal(NULL, TRUE, &ipStreamPtr1);
if (ipStreamPtr1 == NULL) return E_FAIL;
IObjectStreamPtr ipObjectStream1;

45

hr = ipObjectStream.CreateInstance(CLSID_ObjectStream);
if (FAILED(hr)) return hr;
hr = ipObjectStream1->putref_Stream(ipStreamPtr1);
if (FAILED(hr)) return hr;
if (FAILED(::SaveObject((IUnknown*)((IClone*)this), ipObjectStream1, FALSE)))
return S_OK;
Create a second memory stream and save pOther to this stream.
[Visual C++]

IStreamPtr ipStreamPtr2;
::CreateStreamOnHGlobal(NULL, TRUE, &ipStreamPtr2);
...
if (FAILED(::SaveObject(pOther, ipObjectStream2, FALSE)))
return S_OK;
Reset both the streams to the beginning.
[Visual C++]

ULARGE_INTEGER newPosition;
LARGE_INTEGER moveTo;
moveTo.QuadPart = 0;
if (FAILED(ipObjectStream1->Seek(moveTo, STREAM_SEEK_SET, &newPosition)))
return S_OK;
if (FAILED(ipObjectStream2->Seek(moveTo, STREAM_SEEK_SET, &newPosition)))
return S_OK;
Now you can begin to compare the streams. First, compare their size by using the IStream::Stat method to get
statistical information about each stream. If the size of the streams is not equal, exit the IsEqual method.
[Visual C++]

STATSTG ss1, ss2;


ipObjectStream1->Stat(&ss1, STATFLAG_NONAME);
ipObjectStream2->Stat(&ss2, STATFLAG_NONAME);
if (ss1.cbSize.QuadPart != ss2.cbSize.QuadPart)
return S_OK;
Now you can compare the streams, first integer by integer. If the values are not equal, exit the IsEqual method.
[Visual C++]

long l1, l2;


long size = sizeof(l1);
long numInts = (long)ss1.cbSize.QuadPart / size;
DWORD bytesRead;
for (long i = 0; i < numInts; i++)
{
if (FAILED(ipObjectStream1->Read(&l1, size, &bytesRead)))
return S_OK;
if (FAILED(ipObjectStream2->Read(&l2, size, &bytesRead)))
return S_OK;
if (l1 != l2)
return S_OK;
}
Finish by comparing the streams byte by byte.
[Visual C++]

BYTE b1, b2;


long numBytes = (long)ss1.cbSize.QuadPart - (numInts * size);
for (i = 0; i < numBytes; i++)
{
if (FAILED(ipObjectStream1->Read(&b1, 1, &bytesRead)))
return S_OK;
if (FAILED(ipObjectStream2->Read(&b2, 1, &bytesRead)))
return S_OK;
if (b1 != b2)
return S_OK;

46

}
If you have reached this point, the current instance of the class and the instance referenced by pOther are equal, so
set the return value to true, and exit.
[Visual C++]

*pbEqual = VARIANT_TRUE;
return S_OK;
}
The IsIdentical method can be implemented by a simple pointer comparison.

Implementing Persistence
About Persistence
Persistence is a general term, referring to the process by which information indicating the current state of an object is
written to a persistent storage medium such as a file on disk.
Persistence is used in ArcGIS to save the current state of documents and templates. By interacting with the ArcGIS
user interface, you can change the properties of many of the objects that belong to a map document, for example, a
renderer. When the map document is saved and closed, the instance of the renderer class is terminated; when the
document is reopened, you can see that the state of the renderer object has been preserved.
Structured storage, compound files, documents, and streams
Map documents and their contents are saved using a technique known as structured storage.
Structured storage is one implementation of persistence defined by a number of standard COM interfaces. Prior to
structured storage, only a single file pointer was used to access a file. In structured storage however, a compound file
model is used, whereby each file contains storage objects and streams. Storage objects provide structurelike folders
on your operating system, they can contain other storage and stream objects. Stream objects provide storagelike
traditional files, they can contain any type of data in any internal structure. When the stream is later reopened, a new
object can be initialized and its state set from the information in the stream, re-creating the state of the previous
object.
In this way, a single compound file can act as a mini file systemit can be accessed by many file pointers. Benefits of
structured storage include incremental file read/write and a standardization of file structure, although larger file sizes
may also result.
ArcGIS uses structured storage to persist the current state of all the objects used by an application, although other
persistence techniques are also used. Structured storage is only used for non-GIS data.

Persistence in ArcGIS
The structured storage interfaces specified by COM are implemented extensively throughout the ArcGIS framework.
Understanding when persistence is used within the ArcGIS framework will help you to implement correct persistence
behavior in classes you create. The following sections explain when to implement persistence, which interfaces to
implement, and also review a number of issues that you may encounter when persisting objects.
Although persistence is used throughout the ArcGIS framework, it is not ubiquitousnot every object will always be
given the opportunity to persist itself.
Documents
ArcGIS applications use the compound document structure to store documentsmap documents, map templates,
normal templates, and scene documents. All the objects currently running within a document or template are persisted
to streams in the compound file when the document is saved.
Take the example of a map documentwhen a user chooses Save in ArcMap, the MxApplication first creates streams
as required, associates them with the existing .mxd file (if the document has previously been saved), then asks the
document to persist itself to these streams. If there are changes to the normal template or map template, then this
process is repeated for the appropriate .mxt file. This process allows the current state of a document to be recreated
when the file is reopened.

ArcMap, for example, will persist many itemsnotable areas that may include custom objects are noted below.

The map collectioneach Map will persist its layers, symbology, graphics, current extent, spatial reference, and
so on. This may include custom layers, renderers, symbols, elements, or other map items.

The page layout, its Map frames, map surrounds, the layout of items, and so onthis may include custom map
surrounds or frames.

The visible table of contents (TOC) views and their statethis may include a custom TOC view.

The toolbars currently visible, their members, and their position if floating, including standard and custom
toolbars and commands and UIControls.

The registered extensions, and their statethis may include custom extensions.

The current DataWindows, their type, location, and contentsthis may include a custom DataWindow.

A list of the styles currently referenced by the StyleGallery. Items are stored in a style by using persistencethis
could include a custom StyleGalleryItem or StyleGalleryClass.

47

From ArcGIS 9.1, you can save map documents so you can open and work with them in previous versions of ArcGIS.
See the later sections of this topic on version compatibility for more information on handling this kind of persistence in
your custom components.
If any object referenced by the map document is expected to support persistence and does not, errors may be raised
to a user and the completion of the save may be prevented, rendering the document unusable.
You should, therefore, always be clear whether or not your class needs to implement persistence, and implement
correct persistence behavior if required.

Persistable classes
When an object is asked to persist itself, it will write the current value of its member variables to the stream. If one of
the members references another object, and that object is also persistable, it will most likely delegate the persistence
work by asking the member object to persist itself. This 'cascading' effect ensures that all the referenced objects are
given a chance to persistthis may include your own custom objects, if they are referenced by an object that is
persisted.
A persistence event 'cascades' through the document, as each object asks its members to persist
themselves in turn.

As seen previously in document persistence, each class decides what defines its own state and persists only this data
(in most cases, the values of its private member variables).
If for some reason you decide your custom class does not need to save any information about its state to the stream,
but is expected to support persistence, then you still must implement persistence, although you don't necessarily need
to write any data to the stream.
For most custom classes you will create, objects will be persisted to one of the streams created by ArcMap; it is
unlikely you will need to create a new storage or stream yourself.
Extensions
During the Save process, the application checks all currently loaded extensions to see if they implement persistence. If
so, each extension is asked to persist itself. An extension, therefore, does not necessarily have to support
persistenceno errors will be raised if it does notit depends on whether or not the extension needs to persist the
state when a document is closed. Extensions are persisted in the order they are referenced, which is the order of their
CLSIDs.
The Application object creates a separate stream for the persistence of each extension; the new streams are stored in
the same compound file as the other document streams. A separate ObjectStream is also created for the extension
see below for more information about ObjectStreams.

ObjectStreams
An object's state is not always defined by value typesyou have already seen how an MxDocument persists itself by
calling other objects to persist themselves.
Often multiple references are held to the same object, for example, the same Layer in a Map may be referenced by
IMap::Layer and ILegendItem::Layer. If each of these properties were called to persist, two separate copies of the
Layer would be persisted in different sections of the stream. This would bloat file size and would also corrupt object
references.
To avoid this problem, ObjectStreams are used in ArcObjects to persist objects and maintain object references
correctly when persisted.
When an ArcObjects object initiates a persist, that object will create a stream for the persistence. It will also create an
ObjectStream, and associate it with the stream; one ObjectStream can be associated with one or more streams. The
ObjectStream maintains a list of objects that have been persisted to that stream.
The first time a particular object is encountered, it is persisted in the usual manner. If the same object is encountered
again, the ObjectStream will ensure that instead of persisting the object a second time, a reference to the existing
saved object is stored instead.

48

In addition to ensuring the integrity of object references, this helps to keep file sizes to a minimum. Only COM objects,
supporting IUnknown and IPersist can be stored in this way.

Implementing Persistence
To create a persistable class, you should implement either IPersist and IPersistStream or IPersistVariant. Both
interfaces specify three basic pieces of functionality.

Identify the class that is being persisted using the IPersistStream::GetClassID and IPersistVariant::ID
properties.

Save data from an object to a stream using the Save method on either interface.

Retrieve data from a stream and set the members of an object from that data, using the Load method on either
interface.

The choice of IPersistStream or IPersistVariant depends on the development environment you are usingthese
interfaces are discussed in turn below. You do not need to implement both interfaces.
When a document is persisted, the client writes the identity of the class to the stream (using ID or GetClassID). Then
it calls the Save method to write the actual class data to the stream. When a document is loaded, the identity is read
first, allowing an instance of the correct coclass to be created. At this point, the rest of the persisted data can be
loaded into the new instance of the class.
If you wish to implement version-specific persistence code, see the Version Compatibility section later in this topic for
more information.
What needs to be saved?
When you implement a persistent class, the decision of what constitutes the persistent state for your class is yours to
makeexactly what data you choose to write to a stream is up to you.
Ensuring that your code can re-create the state of an instance may include storing data about public properties and
any internal members of the class.
You may decide that certain items of state are not persisted. For example, a Map does not persist the
IMap::SelectedLayer property; upon opening a map document, the SelectedLayer property is null. You should also
decide exactly how a newly instantiated instance of the class is initialized from the data stored in the stream.
Implementing IPersistVariant
The IPersistVariant interface should be implemented by Visual Basic classes that need to be persistable. This interface
was specifically designed for use by VB programmers.
[Visual Basic 6]

Implements esriSystem.IPersistVariant
In the ID property, create a UID and set the object to the fully qualified class name of your class.
[Visual Basic 6]

Private Property Get IPersistVariant_ID() As esriSystem.IUID


Dim pUID As New esriSystem.UID
pUID.Value = "<LibraryName>.<ClassName>" ' or a GUID
Set IPersistVariant_ID = pUID
End Property
A basic implementation of Save and Load is shown below.
[Visual Basic 6]

Private Sub IPersistVariant_Save(ByVal Stream As esriSystem.IVariantStream)


Stream.Write m_sValue1
Stream.Write m_sValue2
End Sub
Private Sub IPersistVariant_Load(ByVal Stream As esriSystem.IVariantStream)
m_sValue1 = Stream.Read
m_sValue2 = Stream.Read
End Sub

49

In the example, the two string member variables, m_sValue1 and m_sValue2, are persisted. Note that streams are
sequential; the Load method must read the data from the stream in the same order the data was written to the
stream in the Save method. Ensure that your data is saved and loaded in the correct order so that the correct data is
written to the correct member.
Coding the Save and Load methods may be considerably more complex if you have a large, complex class.
The stream passed to the IPersistVariant interface is a specialist stream class which implements IVariantStream. Using
this interface, any value type, or any COM object can be written to a stream. This stream class is internal to
ArcObjects.
The IVariantStream interface allows you to write COM objects and value data types to a stream using
the same semantics.
Implementing IPersist and IPersistStream
If you are developing in VC++, you may already be familiar with the IPersistStream interface, an interface defined as
part of COM which inherits the IPersist base interface. You would generally implement this interface in preference to
IPersistVariant, as it is more flexible, and the VC++ environment offers better support for this interface.
IPersistStream inherits from IPersist, and therefore, cannot be implemented in VB. Neither is it suitable for use by VB
clients, as it also uses data types not supported by VB.
IPersist and IPersistStream are standard COM persistence interfaces and can be implemented in
VC++.
A simple implementation of Save and Load, shown below, persists a string (m_bstrValue) and a long (m_lNum). The
code makes use of the WriteToStream and ReadFromStream methods that are available on the CComBSTR smart type.
These methods are also available on CComVariant.
[Visual C++]

STDMETHODIMP CPersistClass::Save(IStream * pStm, BOOL fClearDirty)


{
if (m_bDirty)
{
m_bstrValue.WriteToStream(pStm);
pStm->Write(&m_longValue, sizeof(m_longValue), 0);
}
// reset dirty flag
m_bDirty = false;
return S_OK;
}
[Visual C++]

STDMETHODIMP CPersistClass::Load(IStream * pStm)


{
m_bstrValue1.m_str = NULL;
m_bstrValue1.ReadFromStream(pStm);
pStm->Read(&m_longValue, sizeof(m_longValue), 0);
m_bLoadedSettings = true;
return S_OK;
}
The Save method only writes to the stream if the parameter m_bDirty indicates that the object has changed since the
last save.
The IPersistStream interface includes the IsDirty method, indicating whether or not an object has changed since it was
last saved. The private member, m_bDirty, is used by this class to indicate the state of the object and is set to True
when any change is made to the class and reset as False at the end of the Save method.
The GetSizeMax method should return the maximum size of the persisted data. In many cases it is not possible to
calculate this in advance, for example, when persisting a variable size array or collection. In this case, return
E_NOTIMPL.
[Visual C++]

STDMETHODIMP MyClass::GetSizeMax(_ULARGE_INTEGER * pcbSize)


{
if (pcbSize == NULL)
return E_POINTER;
return E_NOTIMPL;
}
IPersistStreamInit

50

The IPersistStreamInit interface is an alternative to IPersistStream. It provides one extra method, InitNew, which
clients may call to initialize the object to default values. This interface is more relevant to persistence of ActiveX
controls where there is a large range of possible clientsyou will not need to implement this interface for a custom
class for ArcGIS.
IPersistStorage
The IPersistStorage interface is implemented by objects that persist themselves directly to a structured storage
container, rather than a stream. MXD documents are implemented with this kind of persistence. This method is
essential for objects which are to be embedded in OLE containers such as Microsoft Word. You will not need to
implement this interface to create a persistable custom ArcGIS class.
Identifying the document version
If your object can be saved to a previous version of ArcGIS, but you need to account for this in your persistence code
by having different persistence code for different ArcGIS versions, then you should adapt your implementation of
IPersistVariant or IPersist/IPersistStream to identify the document version that your component is being persisted to.
Within a call to load or save, you can find out the version of the document by QIing to the IDocumentVersion interface
on the stream object as shown below; this applies to both the variant stream reference passed to a VB6 component
implementing IPersistVariant, and also to the object stream reference passed to a VC++ component implementing
IPersist/IPersistStream.
[Visual Basic 6]

Private Sub IPersistVariant_Save(ByVal Stream As esriSystem.IVariantStream)


If TypeOf Stream Is IDocumentVersion Then
Dim pDocVersion As IDocumentVersion
Set pDocVersion = Stream
If pDocVersion.DocumentVersion = esriArcGISVersion83 Then
' Load object as 8.3 version of itself.
Else
' Load object.
End If
Else
' Installed client must be 9.0 or previous version.
...
[Visual C++]

STDMETHODIMP CPersistClass::Load(IStream * pStm)


{
IDocumentVersionPtr ipDocumentVersion = pStm;
if (ipDocumentVersion!=0)
{
enum esriArcGISVersion docVer;
ipDocumentVersion->get_DocumentVersion(&docVer);
if (docVer == esriArcGISVersion83)
{
// Load object as 8.3 version of itself
}
else
{
// Load object.
}
else
{
// Installed client must be 9.0 or previous version.
If your code may be installed to machines with an installation of ArcGIS previous to 9.1, then you cannot guarantee
that the stream passed to the persistence methods will support IDocumentVersion. As shown above, you should
always QI for this interface and take appropriate action if this interface is not found. You may wish to provide your
own functions to discover the installed version of ArcGIS, or may wish to rely on your own internal persistence version
number - see the Coding Backward Compatibility section below for more details.

Techniques for persisting different data


The following sections give advice on persisting certain types of data to a stream for implementors of both
IPersistVariant and IPersistStream.
Persisting objects
If you are using IPersistVariant and working in VB, coding the persistence of an object is syntactically the same as
coding the persistence of a value type. When you pass an object reference like this, the stream uses the

51

ObjectStream, associated internally with the stream to persist the object.


[Visual Basic 6]

Stream.Write m_pMyColorObject
The object is reloaded in a similar way.
[Visual Basic 6]

Dim pColor as IColorSet pColor = Stream.Read


If using the IPersistStream interface in Visual C++, a few more lines of code are required. First, you should check if an
ObjectStream is associated with the stream by performing a QI for IObjectStream. If the QI succeeds, you can call
SaveObject on the IObjectStream pointer, passing the object you wish to persist directly to the ObjectStream.
[Visual C++]

IObjectStreamPtr ipObjectStream(pStream);
if (ipObjectStream !=0)
{
HRESULT hr;
hr = ipObjectStream->SaveObject(m_ipColor);
if (FAILED(hr)) return hr;
}
Always check the return value, as if the save fails, this can produce a corrupt stream. If you are working within the
ArcGIS framework, an ObjectStream will already be associated with the stream you receive. Again, be sure to check
the return value when you reload the object.
[Visual C++]

IObjectStreamPtr ipObjectStream(CLSID_ObjectStream);
ipObjectStream->putref_Stream(pStm);
hr = ipObjectStream->LoadObject((GUID*) &IID_IColor, 0, &pUnk);
if (FAILED(hr))
return hr;
Persisting arrays
Often, a class member may be a dynamic array having a variable number of members. In this case, write the value of
the member directly to a stream in its entirety, as it is not a COM object.
You can write each array member in turn to the stream, as long as you include extra information about the size of the
array, since the Load method needs to be able to size the array and read the correct number of members from the
stream to assign to the array.
The example code below demonstrates how this technique can be used in VB, where the variable m_pArrayMember is
a member of the class and also a dynamic array.
[Visual Basic 6]

Dim i As Long, lCount As Long


lCount = UBound(m_pArrayMember) + 1
Stream.Write lCount
For i = 0 To lCount - 1
Stream.Write m_pArrayMember(i)
Next i
The array can now be initialized correctly in the Load method.
[Visual Basic 6]

Dim i as Long, lCount as Long


lCount = Stream.Read
ReDim m_pArrayMember(lCount -1)
For i = 0 to lCount - 1
m_pArrayMember(i) = Stream.Read
Next i
Instead of using a standard dynamic array, you could store object references in an ESRI Array class and persist each
of these references in the same way (the Array class is not persistable itself).
Persisting a PropertySet
You can make use of the ESRIPropertySet coclass to persist a class's member data, as this class is persistable.
Maximum efficiency will be gained during a save if you already use the PropertySet to store your class data internally.
Some of the persistable examples in the ArcGIS Developer Help show examples of this technique.
Document Versions and Object Streams
The Version Compatibility and Coding Save A Copy functionality sections describe how to deal with the persistence of

52

your object at different versions of ArcGIS. If during your component's persistence code you persist object references,
you should also consider that those objects too need to deal with the document version correctly.
All core ArcObjects deal correctly with document version persistencethey do not implement the
IDocumentVersionSupportGEN interface, but instead deal with this issue internally. If you are persisting an object to
an object stream, all core ArcObjects therefore can be relied upon to either persist correctly regardless of version, or
to convert themselves to suitable replacement objects using methods similar to the
IDocumentVersionSupportGEN::ConvertToSupportedObject method.
Error handling when loading
If you encounter an error when you attempt to read a stream, you must propagate the error to the client. As streams
are sequential, your code should not attempt to continue reading, as the stream pointer will not be positioned
correctly, and therefore, the next value cannot be read correctly.
For this reason, you should always be particularly careful when writing and testing persistence code.
Version compatibility
Review the following section on persistence version compatibilityyou can avoid many errors in your persistence code
if you correctly create backward-compatible components.
Safe loading
In some cases, ArcGIS may be able to continue loading a document despite an error in your code, due to the use of
safe loading techniques.
The effects of the error may vary according to the type of component. For example, if ArcGIS attempts to load a Layer
from a document and fails, ArcMap will continue to load the remainder of the document, but the failed layer will be
missing. You should code your component regardless of this functionality and raise an error to the calling function if
you cannot complete the Load, before exiting the Load function.
Unregistered classes
You are responsible for ensuring that your component is registered on a machine, which may open a document with a
persisted version of your component.

Version Compatibility
If you develop a new version of a persistable component, it is quite likely that you will need to persist additional state
informationthis will mean you need to change the persistence signature of your class. However, your component
may still maintain binary compatibility and have the same ClassID.
By coding your persistence methods to be adaptable from the beginning of your development cycle, you can ensure
your component is compatible with other versions of itself when persisted. This will allow you to fully utilize the ability
when using COM to upgrade a component without needing to recompile the component's clients.
Compatibility in ArcGIS
Custom components should be coded with the version compatibility model of ArcGIS in mind.

Backwards compatibility
ArcGIS document files work on the principle of backward compatibility; probably the most common form of
persistence version compatibility. This means that ArcGIS clients can open documents that were created with an
earlier version of ArcGIS.

Forwards compatibility
It is possible to write forwardly-compatible components, for example, a client can load and save a component
with a more recent version than that with which it was originally compiled. Implementing forward compatibility
requires much care and can give rise to long, complex persistence code.

Although ArcGIS does not implement general forwards compatibility (and therefore this is not generally a requirement
for your components), from ArcGIS 9.1 onwards it is possible for users to save their documents as specific previous
ArcGIS versions, using the Save A Copy command. The saved documents can then be opened with a version of ArcGIS
previous to that with which the document was created. At ArcGIS 9.1, you can only save to ArcGIS 8.3. ArcGIS 9.1
map documents are directly compatible with ArcGIS 9.0, so there is no option to save them to version 9.0 specifically.

If your component works, without recompilation, with both the current ArcGIS version and also to previous ArcGIS

53

versions, then you do not need to adapt your component to ensure 'Save A Copy' functionality.
However, if your object cannot be persisted to a previous version of ArcGIS, you should implement
IDocumentSupportVersionGENthis interface will allow you to provide an alternative object instead. See the Coding
Save A Copy Functionality section below.
If your object can be saved to a previous version of ArcGIS, but you may need to account for this in your persistence
code, then you should adapt your implementation of IPersistVariant or IPersist/IPersistStream to identify the version
being persisted to, and make any necessary changes. You can find out more information on this in the 'Identifying the
document version' section under Implementing Persistence.

Coding backward compatibility in persistence


You will now look at an example of creating a backwardly compatible class, by creating three different versions of the
class. The example code is built up step-by-step, showing you how to code the persistence methods each time. The
code is shown here in VB, although you can use the same principles if you are developing in VC++.
You will create a custom watermark layer, which simply displays a faint picture over a map to indicate map copyright
for exporting or printing purposes.
It can be added programmatically to a map and provides limited layer functionality. For more information on creating
custom layers, see Chapter 4, 'Creating Cartography'.
Version 1
For the first version of your layer class, WatermarkLayer, implement ILayer and ILayer2 to provide basic layer
functionality. You will need to store the following member variables.
[Visual Basic 6]

Private m_sName As String

' ILayer::Name

Private m_bCached As Boolean

' ILayer::Cached

Private m_bVisible As Boolean

' ILayer::Visible

Private m_pDisplayFilter As ITransparencyDisplayFilter ' Used for drawing


Private m_dRatio As Double
' Ratio of picture width to height
Now implement IPersistVariant, as a custom layer must be persistable. Before coding the persistence members of the
WatermarkLayer class, add a private constant called m_iCurrPersistVers. You will use this constant throughout the
persistence code to store the version of the class. As this is the first version of the class, set the constant to 1.
[Visual Basic 6]

Private Const m_iCurrPersistVers As Integer = 1


The use of this value is the key to version compatibilityyou should code the Save and Load methods dependent on
this number. The first thing written to the stream in the Save method is this persistence version value.
[Visual Basic 6]

Private Sub IPersistVariant_Save(ByVal Stream As esriSystem.IVariantStream)


Stream.Write m_iCurrPersistVers
Then you can write the class state to the stream.
[Visual Basic 6]

Stream.Write m_sName
Stream.Write m_bCached
Stream.Write m_bVisible
Stream.Write m_pDisplayFilter
Stream.Write m_dRatio
End Sub

In the Load method, start by reading the version number of the persisted classstore this value in the iSavedVers

54

local variable. If this version number indicates a version of the persisted class that is newer than the current version of
the class (m_iCurrPersistVers), the class will not know how to load the persisted information correctly. If iSavedVers =
0, there is an error somewhere, as the minimum expected value is one. Both cases are errors and may cause a corrupt
streamin these cases, raise an error back to the calling function.
[Visual Basic 6]

Private Sub IPersistVariant_Load(ByVal Stream As esriSystem.IVariantStream)


Dim iSavedVers As Long
iSavedVers = Stream.Read
If (iSavedVers > m_iCurrPersistVers) Or (iSavedVers <= 0) Then
Err.Raise E_FAIL
Exit Sub
End If
As Load may be called sometime after an object has been instantiated, you should ensure initialize default values for
the class at the start of the Load.
[Visual Basic 6]

InitializeMembers
Now you can read the persisted class state and set the members of the current objectafter first checking that the
saved persistence version is the version you expect (this check will come in useful later when you produce a new
version of your component).
[Visual Basic 6]

If iSavedVers = 1 Then
m_sName = Stream.Read
m_bCached = Stream.Read
m_bVisible = Stream.Read
Set m_pDisplayFilter = Stream.Read
m_dRatio = Stream.Read
End If
End Sub
Now that you have the first version of your class, you can compile and deploy the component. At this point, users may
have map documents that contain persisted WatermarkLayer objects.
You can use the AddWMLayer command included in the project to add a new WatermarkLayer to a documentcompile
the project and register the AddWMLayer command to ESRI Mx Commands.
Version 2
You are now asked to add functionality to allow people to scale the size of the watermark image, change its location
relative to the full extent, and also to change the level of transparency.
To achieve these requirements, you must adapt your component. Implement ILayerEffects and return True from the
SupportsTransparency property. Create and implement a new interface, IWatermarkLayer, to add properties to scale
the image and set its relative location. Again, see the sample code for full details of the implementation.
Add new member variables to your class to store ILayerEffects::Transparency and also the values of the members of
IWatermarkLayer.
[Visual Basic 6]

Private m_iTransparency As Integer ' ILayerEffects::Transparency


Private m_ePosition As watermarkPosition
' IWatermarkLayer::Position
Private m_dScale As Double ' IWatermarkLayer::SymbolScale
As the data that needs to be persisted has now changed, increment the persist version number for the class by one.
[Visual Basic 6]

Private Const m_iCurrPersistVers As Integer = 2


When you change the persistence signature of a component, the new component should still read
the original data from the first persistence version if the old version is encountered in Load.
The Save method is updated with the new data being written to the stream after the existing members.
[Visual Basic 6]

Private Sub IPersistVariant_Save(ByVal Stream As esriSystem.IVariantStream)


...
Stream.Write m_dRatio
Stream.Write m_iTransparency
Stream.Write m_ePosition
Stream.Write m_dScale
End Sub

55

Now adapt the Load method to always read from the stream the data saved by both the new and old versions of the
component.
[Visual Basic 6]

If (iSavedVers > 0) And (iSavedVers <= 2) Then


... ' Load first persistence signature
If the data in the persist stream indicates there is new (second version only data), read these values as well.
[Visual Basic 6]

If iSavedVers = 2 Then
m_iTransparency = Stream.Read
m_ePosition = Stream.Read
m_dScale = Stream.Read
End If
Note that if the loaded version does not have this second version data, these member variables (m_iTransparency,
m_dScale, and m_ePosition) are set to default values in the InitializeData routine.
If you have loaded the first version of the persistence pattern, set the second version member
variables to default values. If you have loaded the second version of the persistence pattern, read
the additional members.
Now compile and deploy version 2 of the WatermarkLayer class. At this point, if the new version of the component
encounters an older persisted version, it can load from the persisted data.
Use the ChangeWMPosition and ChangeWMScale commands in the version 2 sample code to change the new
IWaterMarklayer properties; use the ArcMap Effects toolbar Adjust Transparency command to change the transparency
of the WatermarkLayer.
Once the document is saved again (by the version 2 component), the persisted version will be version 2.

Version 3
Finally, your customers ask to be able to specify their own choice of Symbol, as they are not happy with the display of
the WatermarkLayer. They no longer want to be able to turn off the WatermarkLayer.
Create and implement another interface, IWatermarkSymbol, with a Symbol propertythis value must also be
persisted.
[Visual Basic 6]

Private m_pMarker As esriDisplay.IMarkerSymbol


...
Private Property Set IWatermarkSymbol_Symbol(ByVal RHS As esriDisplay.IMarkerSymbol)
Set m_pMarker = RHS
End Property
As this requirement changes the data that needs to be persisted, you should again update the m_iCurrPersistVersion
constant to 3.
[Visual Basic 6]

Private Const m_iCurrPersistVers As Integer = 3


To ensure the WatermarkLayer is always Visible, you can remove the member variable m_bVisible and always return
True from the Visible property. Don't forget to remove all references to m_bVisible from the rest of your code.
[Visual Basic 6]

Private Property Get ILayer_Visible() As Boolean


ILayer_Visible = True
End Property

56

Next, change the Save method. You no longer need to save the Visible value, but you do need to persist the new
Symbol.
[Visual Basic 6]

Private Sub IPersistVariant_Save(CODE id="k">ByVal Stream CODE id="k">As esriSystem.IVariantStream)


Stream.Write m_iCurrPersistVers
Stream.Write m_sName
Stream.Write m_bCached
' Stream.Write m_bVisible
...
Stream.Write m_pMarker
End Sub
Last, update your Load method. You still need to read all the items saved at versions 1 and 2; the boolean Visible
value is no longer required at version 3, so you can discard the value once read; you must still read the value,
however, to advance the stream pointer by the correct amount.
[Visual Basic 6]

If (iSavedVers > 0) And (iSavedVers <= 3) Then


m_sName = Stream.Read
m_bCached = Stream.Read
If (iSavedVers <= 2) Then
Dim bVisible As Boolean
bVisible = Stream.Read
End If
Set m_pDisplayFilter = Stream.Read
m_dRatio = Stream.Read
If iSavedVers >= 2 Then
m_iTransparency = Stream.Read
m_ePosition = Stream.Read
m_dScale = Stream.Read
End If
End If
To complete the new Load method, if the persisted version is 3, read the stream again to set the value of the Symbol
property of IWatermarkSymbol::Symbol.
[Visual Basic 6]

If (iSavedVers = 3) Then
Set m_pMarker = Stream.Read
End If
The WatermarkLayer can now use any MarkerSymbol as its watermark. Attempting to change the visibility of the layer
will not have any effect.

Obsolete persisted data


In many cases, a new component version will require adding new data to a persistence pattern. The WatermarkLayer
example above demonstrates that sometimes a new version of a component may no longer need to save data to the
stream.
In these cases, if your new component encounters the older version persisted to a stream, you should always read the

57

obsolete data valuesotherwise, the stream pointer will be left at the wrong location, and the next value will not be
read correctly. You can discard the obsolete values once read and save only the required data in the new Save
method.

Another possibility is to create a class that does not update to the new persistence pattern if saved by a new version of
the component. This would enable old components to Load the persisted object. Note that the persistence version
number written at the beginning of the Save method should account for which persistence pattern is used.
To make the implementation of persistence versions more straightforward, you may want to consider the use of a
PropertySet. Each version of your component can add more, or different properties as required. The Save and Load
events then only need to persist the current PropertySet. If you choose this approach, you should make sure that all
your class members are set to their default values at the beginning of a Load event, in case the values of certain class
members cannot be found in the current PropertySet.

Coding 'Save A Copy' functionality


To allow your component to persist to a document at a particular version of ArcGIS (for example when a user chooses
the Save A Copy command in ArcMap) you may wish to implement IDocumentVersionSupportGEN. This interface
specifies two pieces of functionality:

Allows a component to indicate whether or not it can be persisted to a particular version of ArcGIS document.

Allows a component to provide a suitable alternative object instead of itself, if that component cannot be
persisted to the specified version.

If ArcGIS cannot QI to IDocumentSupportVersionGEN for a given persistable object, then it will assume that the object
can be persisted, unchanged, to any version of ArcGIS document.
Take, for example, a situation where a custom symbol is applied to a layer in a document at ArcGIS 9.1 and the user
chooses to save a copy of the document to an ArcGIS version 8.3 document. The persistence process for the symbol
will follow these general steps to create the version-specific document:
1.

When ArcMap attempts to persist the symbol object, it will attempt to QI to IDocumentSupportVersionGEN to
determine if the symbol can be saved to an 8.3 document.

2.

If the QI fails, then ArcMap will call the persistence methods of the symbol as normal, assuming the symbol can
be persisted to any version of ArcGIS.

3.

If the QI succeeds, ArcMap will then call the IDocumentSupportVersionGEN::IsSupportedAtVersion method,


passing in a value indicating the ArcGIS version required.

If IsSupportedAtVersion returns true, then ArcGIS will call the persistence methods of the symbol as
normal.

If IsSupportedAtVersion returns false, then ArcGIS will call the


IDocumentSupportVersionGEN::ConvertToSupportedObject method on the symbol. The symbol then
creates a suitable alternative symbol object which can be persisted to the ArcGIS document version
specified. This alternative symbol object is then returned from ConvertToSupportedObject, and ArcGIS will
replace object references to the original symbol with references to this alternative symbol.
A diagram showing this last situation is shown below.

58

Implementing IDocumentVersionSupportGEN
The IsSupportedAtVersion method is where you determine to which ArcGIS document versions your component can be
persisted. Return true or false from this method, depending on the document version indicated by the parameter
passed in to this method. The parameter is an esriArcGISVersion enumeration value.
If, for example, your component can be used equally well at all versions of ArcGIS, you can simply return True from
IsSupportedAtVersionalthough in this case you do not need to implement the interface at all.
If however your component relies upon the presence of core objects or functionality which exists only from 9.0
onwards, then you should return false to indicate that the object cannot be used in an ArcGIS 8.3 document. Take the
custom logo layer example which is explained above; you can prevent the layer from being saved as it is to an 8.3
document using code like that below.
[Visual Basic 6]

Private Function IDocumentVersionSupportGEN_IsSupportedAtVersion(ByVal docVersion As


esriSystem.esriArcGISVersion) As Boolean
If docVersion = esriArcGISVersion83 Then
IDocumentVersionSupportGEN_IsSupportedAtVersion = False
Else
IDocumentVersionSupportGEN_IsSupportedAtVersion = True
End If
End Function
In order to allow users to save a copy of a document with the custom logo layer as an 8.3 document you can then use
code like that below to create a basic RasterLayer as an alternative to the custom logo layer, and return this from
ConvertToSupportedObject. (This may not be an appropriate solution for every custom layer of course, but the
principle can be applied to similar code to create an object appropriate to your own customizations).
[Visual Basic 6]

Private Function IDocumentVersionSupportGEN_ConvertToSupportedObject(ByVal docVersion As


esriSystem.esriArcGISVersion) As Variant
If docVersion = esriSystem.esriArcGISVersion.esriArcGISVersion83 Then
' Note this code relies on the raster dataset file being available at the specified
' location and having an appropriate spatial reference applied.
Dim pWsFact As IWorkspaceFactory, pWs As IRasterWorkspace
Set pWsFact = New RasterWorkspaceFactory
Set pWs = pWsFact.OpenFromFile("C:\MyWorkspace", 0)
Dim pRasterDataset As IRasterDataset
Set pRasterDataset = pWs.OpenRasterDataset("MyLogo.bmp")
If Not pRasterDataset Is Nothing Then
Dim pRasterLy As IRasterLayer
Set pRasterLy = New RasterLayer
pRasterLy.CreateFromDataset pRasterDataset
Set IDocumentVersionSupportGEN_ConvertToSupportedObject = pRasterLy
End If
End If
End Function
Note that for every esriArcGISVersion for which you return false from IsSupportedAtVersion, you should implement a
suitable alternative in ConvertToSupportedObjectif you do not do this then you will prevent users from saving a copy
of a document to that version, and the user may not receive any information about why exactly their attempt to save
a copy failed. Do not return a null reference either, as ArcGIS may attempt to apply this null reference to a property,
which may cause the saved document to become corrupted.

59

Your responsibilities when implementing persistence


Review the notes below for a recap of your responsibilities when creating persistable components.
You control what data to persist
Remember that you control exactly what is written to the stream. If your class needs to persist a reference to another
custom object, you have two options. First, you could implement persistence on the other custom class, and persist
that class as you would any other object.
Alternatively, you could write the members of the secondary class to your persist stream for the primary class.
Although the second option may be simpler, the first method is recommended, as it is more maintainable and scalable.
Error handling and file integrity
If you raise errors in a stream Load event, this may cause the current structured storage (for example, the current
.Mxd file) to be unreadable. You must take care when writing persistence code to ensure you preserve the integrity of
storage files.
ObjectsStreams in extensions
As mentioned previously, a separate ObjectStream is created for each extension. This situation can lead to problems
for components which do not account for this difference.
If you persist an object reference that is already persisted elsewhere in the document and, therefore, to a separate
ObjectStream, this will result in two separate objects being persisted. When the document is reloaded, the object you
persisted will no longer be the same object that was persisted in the main document ObjectStream. You may want to
initialize such objects in the extension startup, rather than in persistence code.
Version Compatibility
You must ensure your components are consistent with the version compatibility used by ArcGIS. Ensure as a minimum
that your components are backwardly compatiblethat a client (for example ArcMap) can open a document that was
created with an earlier version of your component.
You should also consider implementing IDocumentVersionSupportGEN, if required, to ensure your component can be
opened in a document which is saved as a previous version of ArcGIS. For each ArcGIS version that you return False
from IsSupportedAtVersion, you should ensure you return a valid alternative object from ConvertToSupportedObject.

Creating type libraries using IDL


About type libraries
A type library is an essential part of a component, providing information to the compiler about the classes, interfaces,
enumerations, and so on, included in the component.
Type library files have the extension .tlb, although type libraries can also be embedded into other files, for example,
object libraries (.olb) or DLLs. Throughout this section, the term type library may refer to either a .tlb file or to a
library contained in another file.
ArcObjects object libraries
Type information for ArcObjects components is contained in a number of object libraries. Each area of ArcObjects
functionality is contained in a different object libraryfor example, the ArcMap user interface classes, interfaces, and
enumerations are defined in the esriArcMapUI.olb object library.
Why are type libraries necessary?
You need to reference a type library to declare variables or implement interfaces using the types defined in the library.
The type library is then used when your component is compiled to check the type information to allow early binding of
types.
Type libraries also allow type-dependent design-time features to function correctly, such as VB's intellisense and
parameter information.
How are type libraries created?
When you create a component, type library information should be created too. The VB compiler automates the process
of creating a type library, embedding type library information within your compiled DLL or EXE file.
If you are developing in VC++, however, you create Interface Definition Language (IDL) files to define the contents of
the type library, which are compiled into a separate .tlb file when the project is compiled.
Why should I use IDL to create a type library?
Type libraries are aimed at allowing development to cross programming language boundaries. However, certain
incompatibilities exist between the type library information that is created by default and that can be implemented by
different development environments.
Although COM components are accessible from any language, a type library may be required to
make the information contained in the component accessible to other developers.
Regardless of the environment you are using, you can ensure your type library is standardized by writing your type
library information in IDL and compiling this into a type library.
You can create an external type library, regardless of the development environment you are using.

60

This could be necessary if you expect your component to be called by or implemented in other environments by other
developers within or outside your development team. Multiple type libraries may also be used as an organizational tool
to separate interfaces, structures, and enumerations from coclass definitions or to separate public from private
information.
This section covers how you can create a type library by writing IDL, both for a VB and a VC++ project, and the
reasons why you might decide to do this. It covers certain issues of writing IDL that can help you ensure your
components can act as a server to a variety of development environments.
The instructions in this section include the use of the Microsoft Interface Definition Language (MIDL) compiler, and
optionally the OLE COM Object Viewer utility (OLE View). Earlier in this chapter, in the 'Choosing your development
environment' section, you can find information on how you can get these utilities.
Viewing a type library
You can investigate the contents of a type library in many ways.
Type libraries can be viewed in many different ways, for example, the Microsoft OLE View utility, the
ESRI Object Browser, and the developer help system.
The ESRI libraries are described in both VC++ and VB syntax in the ArcObjects component help system.
The Microsoft OLE View utility can be used to view the contents of a type library and unpack the IDL code it contains.
As well as .tlb files, this utility can view the type library information inside a DLL or EXE, an ActiveX control, or an
object library file (.olb).

The ESRI Object Viewer can also be used to view the contents of type libraries in these files. The declarations can be
viewed as IDL, as they would appear on an object diagram, or using VB syntax.

If you are a VB developer, you may be most familiar with the VB Object Browser, which shows a VB interpretation of a
type libraryfor more information on what this implies, see the following section on defining interfaces in IDL.

61

About IDL files


IDL is a language that can be used to describe COM interfaces and other data types. Although based on the C
language, it can only be used to define data typesit cannot be used to implement interfaces or create classes. For
more information on IDL, see the bibliography. Essential IDL by Martin Gudgin may be particularly useful for
programmers working in VB.
The basic 'unit' of IDL is composed of two parts. First, a section within square brackets contains attributes, which
define things such as a Globally Unique IDentifier (GUID), a version number, helpstring, and help context ID number.
[
uuid(764EDFE5-09A7-11D6-8A8E-00104BB6FCCB),
version(1.0),

helpcontext(16),

helpstring("DisplayCommands 1.0 Type Library")


]
This section is directly followed by a named entity to which these attributes apply. The section begins with the entity
name, followed by a section within curly brackets, which defines the entity in detail.
library DISPLAYCOMMANDSLib
{
importlib("stdole32.tlb");

// Import libraries containing

importlib("stdole2.tlb");

// standard COM API calls.

importlib("C:\Program Files\ArcGIS\Com\esriFramework.olb");
[
object,
uuid(764EDFF1-09A7-11D6-8A8E-00104BB6FCCB),
helpstring("IZoomIn Interface"),
pointer_default(unique)
]
interface IZoomIn : IUnknown
In the following pages, you can find more information about issues you may face when creating type libraries in VB
and in VC++ and how you can create more language-independent type libraries for your components.
Creating a type library for a VC++ component
As a VC++ programmer you should already be familiar with the process of creating external type libraries by using IDL
to define an interface and the MIDL compiler to compile the IDL to a type library file.
If you have experience with Active Template Library (ATL), you may have set MIDL compiler options via the Project
Settings dialog box; although as IDL is integrated into the project environment, you may not have been specifically
aware of using it.
The MIDL compiler is used by the VC++ environment to create a type library; the MIDL tab of the
Project Settings dialog box is used to adjust MIDL compiler settings.

This section presents a brief review of the steps to turn IDL into a type library for your project.
Creating an IDL file
The basic creation and editing of an IDL file using the ATL COM AppWizard is described in Development Environments,
COM, Visual C++ section of the developer help system.

62

In summary, there are two steps. First, create a new project using the ATL COM AppWizard. Then use the ATL Object
Wizard to add objects to the project, choosing Simple Object as the template. The interface type you choose, dual or
custom, will depend on the intended usage of the componentsee the 'Coding Interfaces' section earlier in this
chapter for more information. At this point, you have a project containing a basic IDL file defining a library.
Adding members to the IDL file
You can easily add interfaces and implement members on your new class by using the context menus on the Visual
C++ ClassView. However, you should check the IDL file to make sure it matches what is defined in your C++ code. In
particular, check the following.

After using the Implement Interface Wizard to implement interfaces on your new class, add these interfaces to
the definition of the class in the IDL file.

If the interfaces you implemented are defined in another library, for example, the esriFramework object library,
import the library using the importlib directive.

Ensure that any enumerations are defined in your IDL if they may be used by clients.

Editing IDL for client neutrality


If you are creating a component in VC++ that may be used in other environments, for example, the ArcGIS VBA
environment or VB, you should be aware that not every environment supports all the items you can define in VC++
and IDL.
First, you will need to restrict the data types publicly used in your component to those supported in the target
environment. Other issues you may want to consider include:

Interface inheritanceVB cannot implement an interface inherited from another custom interface.

Attributesvarious IDL interface, method, and parameter attributes are not supported by VB. This may also
affect the way your VC++ method calls are structured.

Multiple outbound interfacesVB only supports a single outbound interface to be sinked. You can provide a
solution for this problem purely in IDL.

Return typesonly error HRESULTS can be converted by VB into errors, positive HRESULT information will be
lost in VB.

For more information on how to make these changes, read the 'Editing IDL' appendix. You may also want to refer to
the IDL references included in the bibliography.
Compiling the IDL to a Type Library
When you build your VC++ project, referenced IDL files are compiled to a .tlb file. The compiler switches used can be
modified, if necessary, from the MIDL tab of the Project Settings dialog box.
Removing type library information from the DLL
By default, type information is also included in the DLL as a resource. This can be removed. ArcObjects DLLs contain
no type information, which is instead contained centrally in the object libraries. You may want to remove type library
information from your DLL if you have a particular requirement to reduce the size of the DLL, or to keep type
information separate from your implementation code.
You can compile a DLL without type information by ensuring the type library is not included as a resource. From the
View menu in Visual Studio, open the Resource Includes dialog box and remove the type library directive. It will look
something like the following:
1 TYPELIB "ZoomInSample.tlb"
You can also remove this line directly from the resource (.rc) file if you open it up as a text file.
Alternatively, you can leave type information in your DLL, but prevent that information from being entered to the
system registry when the DLL is registered. You can do this by changing the RegisterSever call in the DllRegisterServer
function of your project to pass a parameter of FALSE.
Type libraries are not always needed at runtime, and therefore, are not always present on an install to a user machine.
However, if your component is dual interface, the type library is needed at runtime to turn IDispatch calls into v-table
calls. For this reason, if your component is dual interface, you should not remove the type library information from the
component DLL.
Creating an external type library for a component created in VB
As a VB developer, you would usually define an interface in a public noncreatable class module by stubbing out the
methods and properties required. The interface is then inherited in a creatable class module using the Implements
keyword.
How VB creates type library information
When a project is compiled, VB includes type library information in the component's binary file (DLL or EXE). VB hides
the details of COM implementation from developers, and many 'under the covers' differences are hidden within the
type library. Although any clients also written in VB will be able to call or implement this interface with no problems,
developers in other environments may encounter problems when trying to use or implement the interfaces defined in
this component.
By default, VB compiles type library information into the component binary files.
For example, VB prefixes all VB-defined interface names with an underscorefor a class module (defining an interface)

63

named IZoomIn, the name in the type library will be _IZoomIn. VB clients will automatically remove this underscore;
however, clients written in other languages may need to use the name as specified in the type library.
Interfaces created in VB are always dualthey will inherit from both IUnknown and IDispatch.
The VB compiler also automatically defines a hidden, default interface for each class, containing all the public members
of the class. Open a VB DLL in OLE View to see the changes that have been made to class names, and so on.
VC++ programmers may use a VB component by using these internal names or by redefining the names using the
import. However, neither approach is ideal, especially if the component is to be distributed to third parties.
If your VB component implements a custom interface, you may need to create an external type
library to provide information about the interface to any non-VB clients that want to use it. This is
because, although VB includes type library information in compiled projects automatically, it is
nonstandard, and may cause problems in other development environments.
Alternatively, the VB programmer may provide a separate type library for the component, allowing all clients to access
the VB component on equal terms.
Creating an external type library for a VB component
If you are familiar with IDL, you can go ahead and write your type definitions in IDL and compile the file into a type
library (.tlb) file.
If you are not familiar with IDL, you can use VB and the Microsoft Visual Studio tools to help write your IDL file for
you.
In summary, the VB environment is used in the usual way to stub out the interfaces in the component and compile a
DLL, then the OLE View utility unpacks the type library from this component. This information can be copied to an IDL
file, then the MIDL compiler is used to compile the IDL file into an external type library, which can be used by other
development environments.
To create an external type library for a VB component, you need to define a type library using IDL.
There are utilities available, which can help you create your IDL.
The following steps describe how to create an external type library for VB by using OLE View and the MIDL compiler.
Creating the basic IDL file with VB and OLE View
Follow the steps below to create an IDL file for a VB project.
1.

In VB, create a new ActiveX DLL project. Define your required interfaces as usualuse a public noncreatable
class module and add public properties and methods. For example, add a class module called IMyInterface, and
define a property:
[Visual Basic 6]

Public Property Get MyValue() As Integer


End Property
2.

Compile the component.

3.

From the Start menu, navigate to Microsoft Visual Studio Tools and open the OLE View utility.

4.

In OLE View, click the View TypeLib menu option, navigate to the component you just built, then click Open.
An .idl file for your component will be displayed in the viewer, looking something like that shown below.
// Generated .IDL file (by the OLE/COM Object Viewer)
// typelib filename: MyProject.dll
[
uuid(E955ED36-19E2-4EE0-8C1C-841D845D00A0), version(1.0)
]
library MyProject
{
// TLib :

// TLib : OLE Automation : {00020430-...46}

importlib("stdole2.tlb");
// Forward declare all types defined in this typelib
interface _IMyInterface;
[
odl, uuid(4509BC45-7691-4B6A-81C6-2C7EAF0C982B),
version(1.0), hidden, dual, nonextensible, oleautomation
]
interface _IMyInterface : IDispatch {
id(0x68030000), propget]
HRESULT MyValue([out, retval] short* );
};
[

64

uuid(FBC41564-3748-445D-A11D-3343178373AD),
version(1.0), noncreatable
]
coclass IMyInterface {
[default] interface _IMyInterface;
};
};
5.

Copy the IDL to a text editor and save the file using the suffix .idl.

Editing the IDL from OLE View


If you try to compile your new IDL file as it stands, it is likely that the MIDL compiler will give errors. If it should
compile successfully, you will find that the contents of the type library are not exactly as you might expect. As you can
see in the IDL shown above, the interface name IMyInterface is changed to _IMyInterface, and there is a coclass
definition for the IMyInterface, which was the interface you defined in a class module.
After creating your IDL file with the OLE View utility, you will need to edit the IDL by hand before
compiling it to a type library.
Before compiling the type library, you will need to make changes to the IDL file, which you can do in any plain text
editor, for example, Notepad.
These changes will correct syntax and changes made by the VB compiler, and also the differences between OLE View
interpreted IDL and IDL syntax required by the MIDL compiler. Briefly, these changes include:

Removing extraneous coclass definitions and default interface definitions if not required.

Correcting interface, structure, and enumeration names.

Correcting coclass, interface, and parameter attributes.

Adding logical parameter names.

Adding helpstrings and help context IDs if necessary (you can do this in VB for class methods only).

Ensuring syntax is correct if creating an IUnknown interface.

For more information on how exactly to make these changes, read the 'Editing IDL'appendix; you may also want to
refer to the IDL references included in the bibliography.
Creating a type library from the IDL
After editing the IDL, you can create a type library. Use the MIDL compiler (midl.exe) to compile the .idl file. (Note
that the "/h" switch will produce a C/C++ header file as well.)
The MIDL compiler is used to turn the IDL you have written into a type library.
If you received syntax errors, check the Visual Studio online reference for more information on IDL and the MIDL
compiler. Check that any changes you made to the IDL conform to the syntax correctly.
You can place the commands in a batch file to make it easier to rerun your compilation as you fix syntax errors, as
shown below.
REM Set variables to point to the correct folders on your computer.
SET MIDLDir="C:\Program Files\Microsoft Visual Studio\VC98\Bin"
SET IDLFile="C:\Temp\IDL\MyLayer.idl"
SET TLBFile="C:\Temp\IDL\MyLayer.tlb"
REM Navigate to the MIDL directory
chdir /d %MIDLDir%
REM run the MIDL compiler producing a TLB file
midl "%IDLFile%" /tlb "%TLBFile%"
pause
A batch file is useful to rerun your MIDL compiler commands, as you may need to recompile a
number of times before you eliminate all syntax errors.
You now have a type library providing type information about your VB component that you can call and implement in
VC++ or other COM-compliant environments.
Implementing the interface in your VB component
You can now return to VB and use this type library in your component.
1.

Open the project where you want to create your component. Add a reference to the type library you just created
by clicking Project, clicking References, and navigating to the .tlb file.

2.

In the class module, as appropriate, implement the interfaces you just defined in your type library by using the
Implements keyword, ensuring the interface name is fully referenced.
[Visual Basic 6]

65

Implements MyLibraryName.IMyInterface
3.

Now stub out and complete the implementation of the interface members.

4.

Ensure that you compile your component using this new reference. If you used the same project to define your
interface in the first place, make sure you remove the interface modules.
By referencing the new standalone type library in your project, you can implement the interfaces it
contains in VB classes, and use any structures or enumerations you defined.

In VB, a coclass cannot be defined based on IDLany coclass definitions in the IDL cannot be reused in VB. However,
enumerations and structures defined in a type library can be used directly in VB code, as long as they contain
compatible data types.
Note also that VB is unable to implement existing outbound interfacesfor example, a VB component cannot be a
source of events defined by the existing IActiveViewEvents interface.

Implementing help for custom classes


Creating a help system
Once you have developed new components for ArcGIS, you may want to provide documentation to help your users
understand how to use the new functionality. The sections below will help you to understand how help is provided in
ArcGIS and how you can provide help for your own components.

Help in ArcGIS Desktop applications


ArcGIS Desktop applications such as ArcCatalog, ArcMap, and ArcScene, provide online user assistance in six contexts.

ArcGIS Desktop Help, accessed by the Help menu or the F1 key.

Context-sensitive help for commands or tools, shown by using the What's This tool or using the Shift+F1 key
combination when a particular item is highlighted.

Context-sensitive help for controls on dialog boxes or on property pages, and also buttons in dialog boxes or
property pages that display more extensive help topics.

66

ArcGIS Developer Help, invoked from the Start menu.

Context-sensitive help for object model components, contained in the ArcGIS Developer Help, invoked from the
Visual Basic for Applications IDE.

Although you cannot merge your own help file with the ArcGIS Desktop Help or the ArcGIS Developer Help, you can
supplement the ESRI-provided help with your own help in all of the situations listed above.

Invoking Compiled Help Files


ArcGIS uses compiled Help files in both WinHelp (.hlp) and HtmlHelp (.chm) formats.
HtmlHelp is used for the majority of help in ArcGISfor example, both ArcGIS Desktop Help and the ArcGIS Developer
Help are HtmlHelp systems. WinHelp is used as a supplementary system for context-sensitive help, as it is able to

67

display formatted text and diagrams in lightweight windows.


HtmlHelp is used for most ArcGIS Help files.
WinHelp is used to provide 'What's This' help in the UI.
In many of the examples below, both formats are used to show the varying syntax and use. However, you need only
use the format that best meets your needs.
First, you should review the basic mechanism used to display HtmlHelp and WinHelpthese basics are common to all
situations where you may want to display a help window.
Displaying HtmlHelp in VB
1.
Write and compile your HtmlHelp fileyou can either use the Html Help Workshop tool, which is part of Visual
Studio 6.0, or any third party HtmlHelp tool.
2.

Add an entry to the Windows registry to indicate the path of the Help file. ESRI's practice is to register HtmlHelp
Help files on installation in
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\HTML Help
You can also register your help files to this location if you want. If you do not register your help files, you can
alternatively pass in the full filename and path when you call the Help display function.

3.

The HtmlHelp function in the Hhctrl.ocx library is used for all interaction with HtmlHelp. Add the following Win32
API declaration for this function to a module if working in VB 6.
[Visual Basic 6]

Declare Function HtmlHelp Lib "hhctrl.ocx" Alias "HtmlHelpA" ( _


ByVal hwndCaller As Long, ByVal pszFile As String, _
ByVal uCommand As Long, ByVal dwData As Long) As Long
4.

Add a declaration for the HH_DISPLAY_TOPIC constant, which informs the HtmlHelp function that it should
display a Help window.
[Visual Basic 6]

Public Const HH_DISPLAY_TOPIC = &H0


5.

Use code similar to that below to call the HtmlHelp function, passing in the window handle of the current
application (hWnd), which is the filename of your help file, the HH_DISPLAY_TOPIC constant.
[Visual Basic 6]

Dim hwndHelp As Long


hwndHelp = HtmlHelp(hWnd, "MyHelpfile.chm::\IntroPage.htm", ;HH_DISPLAY_TOPIC, 0)
This code will display the page called "IntroPage.htm".
Note that no path is specified for the .chm file. The value returned from the function call is the window handle of
the new HtmlHelp window you just created.
Use the Win32 call HtmlHelp to display a HtmlHelp window.
You can also use this API call to open a specific topic in a help file and to execute various other
HtmlHelp commands.
Displaying HTML Help in VC++
In VC++ you use the same HtmlHelp function as described previously for VB. The HtmlHelp header file and library you
will need to use can be found under the HtmlHelp Workshop installation folder.
1.

Begin by compiling and registering the Help file, as described in steps 1 and 2 for VB.

2.

Include the WinHelp.h header file in your own header file.


[Visual C++]

#include <htmlhelp.h>
VC++ users can find the Htmlhelp.h header file as part of the HtmlHelp Workshop installation.
3.

Link to the HtmlHelp.lib import library.

4.

Use code similar to that below to call the HtmlHelp functionthe code assumes that hWnd points to the handle
of the currently active window, and filePath contains the filename of your help file and the topic you want to
display. HH_DISPLAY_TOPIC is defined in HtmlHelp.h.
[Visual C++]

::HtmlHelp(hWnd, filePath,HH_DISPLAY_TOPIC, 0);


Displaying WinHelp in VB
WinHelp is displayed in a similar way to HTML Help.
1.

Write and compile your WinHelp fileyou can either use the Help Workshop tool that is part of Visual Studio or
any third-party WinHelp tool.

2.

Add an entry to the Windows registry to indicate the path of the help file. ESRI's practice is to register WinHelp
files on installation in

68

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\HELP
You can also register your help files to this location if you want. If you do not register your help files, you can
alternatively pass in the full filename and path when you call the Help display function.
3.

The WinHelp function in the Windows User32 library is used for all interaction with WinHelp. Add the following
API declaration to a module if working in VB 6.
[Visual Basic 6]

Public Declare Function WinHelp Lib "user32" Alias "WinHelpA" ( _


ByVal hwnd As Long, ByVal lpHelpFile As String, _
ByVal wCommand As Long, ByVal dwData As Long) As Long
4.

Add a declaration for the HELP_CONTEXT constantthis informs the WinHelp function that it should open a
particular topic.
[Visual Basic 6]

Public Const HELP_CONTEXT = &H1


Use the WinHelp Windows API call to open a WinHelp window, specifying a particular topic ID if
required. WinHelp topic IDs are compiled into the WinHelp file by Help Workshop.
5.

Use code similar to that below to call the WinHelp function, passing in the window handle of the current
application (hWnd), the filename of your help file, the HELP_CONTEXT constant, and the Help Context ID of the
page you want to display.
[Visual Basic 6]

Dim lResult As Long


lResult = WinHelp(Me.hwnd, "MyHelpfile.hlp", HELP_CONTEXT, 100)
The WinHelp function performs essentially the same task as the HtmlHelp function. However, rather than the
name of an HTML page, WinHelp requires that you pass the topic ID's mapped numeric value.
The topic ID is the so-called # footnote in the RTF file you include in your WinHelp file. You can use Help
Workshop to add this numeric value by opening the HPJ file, clicking Map, then clicking Add (to add the mapping
for an individual topic). See the Help topic "To enable a program to display an individual Help topic" in the Help
Workshop's Help file.
The value returned from the function call is the window handle of the new HtmlHelp window you just created.
Displaying WinHelp in VC++
Calls to WinHelp are straightforward in VC++, as they are part of the Windows API.
As the WinHelp API call is part of the Windows API, VC++ programmers can call the function
directly.
1.

Begin by compiling and registering the Help file, as described in steps 1 and 2 for VB.

2.

Use code similar to that below to call the WinHelp functionthe code assumes that hWnd is the handle of the
currently active window, filePath points to a string that contains the filename of your help file, and helpId is a
double word containing the topic ID of the topic you want to display.
[Visual C++]

::WinHelp(hWnd, filePath, HELP_CONTEXT, helpId);


Filepaths
No filepath is specified in either of the examples aboveas the help filenames and paths were added to the registry,
the HtmlHelp and WinHelp functions are able to locate the installed path of the files.
Alternatively, you can specify the full path to the help file in either the HtmlHelp or WinHelp functions.
HtmlHelp hWnd, "C:\Program Files\ABC\MyHelpfile.chm", HH_DISPLAY_TOPIC, 0)
WinHelp(Me.hwnd, "C:\Program Files\ABC\MyHelpfile.hlp", HELP_CONTEXT, 10)
By adding help file information to the windows registry, you can open help files without having to
know the pathnames of the files.
The method you choose to use depends on your requirements. If you create an installation program for your
component, it may be simpler to write the registry values during the installation than working out the path at run time
in your component.

Displaying help for your component


Below is more information on the various places you may want to provide access to help.
Invoking help from ArcGIS applications
As you cannot merge your own help file with the ArcGIS Desktop Help, you can instead add a command that invokes
your compiled help file to the Help menu of an application.

69

You can add a command to the ArcGIS UI to open your main help file.
First, create a class that implements ICommand. Then in its OnClick method, invoke your help file by calling the
HtmlHelp (or WinHelp) functions, as described previously. Add this command to the application's Help menu.
Invoking help from a standalone application
If you are creating a standalone application, you can also use the code shown previously to invoke help files from
menu items or buttons when your application requires it.
Help files can be invoked from standalone applications. To conform with Windows standards, ensure
any Help windows opened by your application are closed when the application exits.
To be consistent with other applications, a user would expect that exiting your application would also close any Help
windows that were created either directly or indirectly by your application. To close any HtmlHelp windows that were
opened by your program, the HtmlHelp function can be called using the HH_CLOSE_ALL constant. In VB, you would
use this in the QueryUnload method of a VB form.
[Visual Basic 6]

Private Const HH_CLOSE_ALL = &H12


Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
HtmlHelp hWnd, "", HH_CLOSE_ALL, 0&
End Sub
More information on closing all HtmlHelp windows can be found on the Web sites listed below, particularly the
Helpware Web site.
To close a WinHelp window opened by your program, use the HELP_QUIT constant, passing in the name of the
WinHelp file, as shown below in VB.
[Visual Basic 6]

Public Const HELP_QUIT = &H2


...
WinHelp Me.hwnd, "MyHelpfile.hlp", HELP_QUIT, 0
If you opened more than one WinHelp file during execution, you should quit each file individually.
There are several newsgroups and Web sites devoted to tips and techniques. The Web sites listed below are notable
parts of an active help user community and provide excellent reference material for developers.

Microsoft Help authoring newsgroup:


Microsoft.public.helpauthoring

Helpware Web site:


http://www.helpware.net

Help Technology Centre Web site:


http://www.mvps.org/htmlhelpcenter
There are several newsgroups and Web sites you may want to go to for more indepth information on
creating and calling help systems.

Displaying help for dialog boxes and property pages


In ArcGIS, there is sometimes the requirement to provide help for a certain command or wizard, more than a simple
ToolTipit may contain many paragraphs of text and, therefore, require a scrollable window, or it may include
diagrams or hyperlinks.
In ArcGIS, a secondary window is used for this type of help page; a secondary window, unlike a main window, does
not have a menu bar. For example, the GeoProcessing wizard shown here includes a button to explain more about the
process that is selected in the wizard.

70

Secondary Help windows are often used to display certain topics when you do not wish to open a full
help file. Both WinHelp and HtmlHelp topics can be opened as secondary windows.
You can use either the WinHelp or HtmlHelp functions to display a WinHelp or HtmlHelp window as you did before. For
example, to display a specific page in HtmlHelp, use the HELP_CONTEXT topic and pass in the name of the page you
want to display for the current state of the dialog box.
To specify that a WinHelp page should display in a secondary window instead of the main Help window, add a greater
than sign (>) and the name of the secondary window to the end of the help filename.
[Visual Basic 6]

lResult = WinHelp(Me.hwnd, "MyHelpfile.hlp>DialogHelp", HELP_CONTEXT, 10)


You can use similar syntax with the HtmlHelp function.
[Visual Basic 6]

hwndHelp = HtmlHelp(hWnd, "MyHelpfile.chm::\IntroHelpPage.htm>DialogHelp", _


HH_DISPLAY_TOPIC, 0)
In both of the cases above, the named secondary window should be defined in the HtmlHelp project or WinHelp
project, respectively. You can use Help Workshop or HtmlHelp Workshop to define the name of the window and its
appearance (color, size and initial position, and window title).
What's This Help for Commands and Tools
In an ArcGIS application, a user can display What's This help for a command or tool in two ways.

By selecting the What's This tool and clicking another command or tool.

By highlighting a command or tool and pressing Shift+F1


'What's This' help windows are opened from the What's This tool in the ArcGIS UI or by pressing
Shift+F1. These are lightweight windows with no menu, scrollbars, or frame.

What's This help is displayed in a popup windowthis is a lightweight window that does not have a menu bar, menu,
frame, or scrollbar. It disappears upon any subsequent mouse click or key press.

The HelpFile and HelpContextID properties of ICommand are used to link a custom command to the What's This help
in the ArcGIS applications.
Using WinHelp for What's This help for a command
ArcGIS applications use WinHelp files to display What's This help, as WinHelp supports formatted text and graphics for
popup windows. The example code below for ICommand demonstrates calling WinHelp for What's This help for a
custom command.
[Visual Basic 6]

Private Property Get ICommand_HelpFile() As String


ICommand_HelpFile = "MyHelp.hlp"

71

End Property
Private Property Get ICommand_HelpContextID() As Long
ICommand_HelpContextID =
End Property

1234

What's This help for a command is opened automatically if you have correctly specified the
ICommand members HelpFile and HelpContextID.
Topics can be linked to an ID number either directly in Help Workshop or by using a text file listing topic name and ID
number. The actual contents of each topic should be included in an RTF file, which is compiled into the Help file.
Using HtmlHelp for What's This help for a command
It is also possible to specify an HtmlHelp topic for What's This help for a command, if required. Note that HtmlHelp
does not support formatted text or pictures for popup windows. For the ICommand::Helpfile property, specify the
name of the HtmlHelp .chm file.
[Visual Basic 6]

Private Property Get ICommand_HelpFile() As String


ICommand_HelpFile = "MyHelp.chm"
End Property
WinHelp What's This help windows support the addition of formatted text and graphicsHtmlHelp
What's This help windows do not support either.
Generally calling HtmlHelp, you would specify a topic namehowever, for the HelpContextID property, you must pass
a symbolic ID number. Symbolic ID numbers should be defined in a header file in your HtmlHelp project, along with
the topics to which they relate.
The actual contents of the popup windows should be specified in a text file; see the HtmlHelp Workshop's online
reference for information on creating popup topics.
If you require more information about the Helpfile and HelpContextID properties of ICommand, refer to the ArcGIS
Developer Help.
Displaying context-sensitive help for controls on a property page
Many of the property pages in the ArcGIS Desktop applications have context-sensitive help for individual controls on
these property pages. Popup help topics appear when the control has focus and the user presses F1 or selects the
What's This menu button and clicks a control.
You can provide context-sensitive popup help for your own property pages by linking the controls on your property
page dialog box or form to ID numbers specified in your help file.
The IComPropertyPage and IComPropertyPage2 interfaces contain two properties, HelpFile and HelpContextID. These
properties perform the same roles as the properties of the same name on the ICommand interface. Respectively, they
specify the name of the help file and the numeric value of the topic ID for each control.
When providing context-sensitive help for controls, ESRI's practice again is to use a WinHelp file.

Context-sensitive help for custom property pages is implemented via the standard property page
interfaces. It is ESRI's practice to provide What's This help windows as WinHelp topics.
Context-sensitive help for controls on a property page in VB
To add What's This help to a property page implemented in VB, select each control on the Form in turn, press F4 to
display the Properties Window, then enter the numeric Help Context ID for the WhatsThisHelp property.
In VB, the property page What's This help can be linked to specific topics by using the
WhatsThisHelpID control property in conjunction with the IComPropertyPage members HelpFile and
HelpContextID.
Once all values are assigned, add the following code to the implementation of the IComPropertyPage interface.
[Visual Basic 6]

72

Private Property Get IComPropertyPage_HelpFile() As String


IComPropertyPage_HelpFile = "MyHelpfile.hlp"
End Property
Private Property Get IComPropertyPage_HelpContextID(ByVal controlID _
As Long) As Long
IComPropertyPage_HelpContextID = m_pFrm.Controls(controlID - 1).WhatsThisHelpID
End Property
When the property page is displayed in a property sheet, the property sheet will forward the call to What's This help to
these methods and display the topic specified by the context ID number and the help file.
If you intend to use HtmlHelp to provide popup help, see the previous notes about specifying symbolic ID numbers for
HtmlHelp topics. You should also note that HtmlHelp does not support formatted text or diagrams in popup help
windows.
Context-sensitive help for controls on a property page in VC++
When you add controls to the dialog box for a property page, ensure the Help ID check box in the control's Properties
dialog box is checked. This will ensure resource ID numbers are defined for each control in the resource header file.
What's This help for individual controls on a property page implemented in VC++ can be linked to
specific topics by using the Help ID control property in conjunction with the IComPropertyPage
members HelpFile and HelpContextID.
Now complete the implementation of IComPropertyPage by returning the appropriate Helpfile string from the
implementation of the IComPropertyPage interface. Use the controlIDs for the return value of GetHelpID.
[Visual C++]

STDMETHODIMP CMyPropPage::GetHelpFile(LONG controlID, BSTR * HelpFile)


{
if (HelpFile == NULL)
return E_POINTER;
*HelpFile = ::SysAllocString(OLESTR("MyHelpfile.hlp"));
return S_OK;
}
STDMETHODIMP CMyPropPage::GetHelpId(LONG controlID, LONG * helpID)
{
if (helpID == NULL)
return E_POINTER;
*helpID = controlID;
return S_OK;
}

Error handling in components


How COM handles errors
Most development environments and programming languages have their own error handling model. COM was designed
to be language-neutral, and therefore, needs a language-neutral error handling model. The COM specification defines
two parts to the COM error handling model.
COM error object
A method call in a COM server that has generated an exception creates an instance of the COM error object using the
ICreateErrorInfo interface. The server then populates the error object with information about the source and cause of
an error. When program control returns to the COM client, the client can retrieve the error object, find the information,
and use this information determine how to handle the error.
Unlike exceptions thrown within a program, COM exceptions do not stop the flow of execution; instead, a COM error
object is created within the current thread. The error object supports the IErrorInfo interface, which has methods
allowing the client to get the name of the class and interface that created the error and a description of the error.
COM defines another interface, ISupportErrorInfo, which is used to indicate that a class may create a COM error
object. Many ArcObjects classes implement the ISupportErrorInfo interface.
About HRESULTS
As the COM error object does not halt program flow, there needs to be some mechanism by which the client can tell
when an error object has been createdCOM's answer to this is the HRESULT.

73

The COM specification states that all function calls should return an HRESULTa 32-bit unsigned integer, which indicates
success or failure of the call. An HRESULT contains various pieces of information.

The first bit indicates method success or failure (0 indicates success, 1 indicates failure) and is called the
severity code.

The lower 16 bits contain an error code specific to a component or application, which is referred to as the
information code.

Information about the context of an error is contained in the remaining bits and is referred to as the facility code

Identifying an error
HRESULT values are often written using hexidecimal (base 16) notationsome of the most common standard
HRESULTS are shown in the table below.
Symbolic Constant

Hexidecimal
Value

Description

S_OK

00000000

Standard return value indicating successful completion

S_FALSE

00000001

Alternate success value, indicating successful but nonstandard completion


(precise meaning depends on context)

E_UNEXPECTED

8000FFFF

Catastrophic failure

E_NOTIMPL

80004001

Not implemented

E_OUTOFMEMORY

8007000E

Out of memory

E_INVALIDARG

80070057

One or more arguments are not valid

E_NOINTERFACE

80004002

Interface not supported

E_POINTER

80004003

Pointer not valid

E_HANDLE

80070006

Handle not valid

E_ABORT

80004004

Operation aborted

E_FAIL

80004005

Unspecified error

E_ACCESSDENIED
80070005
General access denied
If you are working in VC++, you can find a listing of standard Windows and COM HRESULTS and their associated
constants in the WinError.h header file, which is installed with Visual Studio. If you are working in VB, you can look up
these HRESULTs in MSDN, although you should be aware that the VBVM may translate certain HRESULTS into VBspecific error codes; see the following sections for more information on VB error codes.
Facility Codes
The facility code 4 indicates an error is caused by a call to a COM interface. Errors created by ArcObjects interface calls
generally use a facility code of 4, although there are exceptions to this rulefor example, the Engine Controls use the
facility code 10 (FACILITY_CONTROL). A list of standard facility codes can also be found in the WinError.h file or in
MSDN.
ArcObjects error codes
A number of enumerations, defined in ArcObjects libraries, give the error codes and description string of errors which
may be created by ArcObjects method calls.

ArcObjects libraries contain enumerations of constants listing error codes.

74

Error handling in components


The way in which a component handles errors may differ from the way in which an application handles errors.
User-interface components, such as property pages and dialog boxes, can make use of error-handling routines to
check user input to the interface and to allow a user to correct an error. However, many of the examples presented
throughout this book demonstrate classes without a user interfacefor example, a workspace extension, or a custom
symbol without a property page. For such classes you should always avoid using UI features such as message boxes or
forms in your error handling routines. Also, when designing your error handling routines, you should always consider
the context in which your component may be runningcould your component be instantiated in a server environment?
If you cannot handle an error created in your component (or raised from a server component), then you should pass
the error back to your client in turn. In some cases, you may want to translate the error to a more appropriate error
for your component. For example, a custom layer component may experience a problem reading from the disk;
however, the layer client would find it more useful to receive an error indicating that the layer cannot be displayed,
rather than a low-level disk or file error.
Specifying error codes for your components
You can define your own range of error codes, allowing clients to see which error codes may be raised by your
component. You can expose your error codes in a similar way as used by ArcObjectsby declaring enumerations of
constants, giving an error code and symbolic constant.
As errors created by your custom components will be created when the client calls an interface member, your
HRESULTs should use the facility code 4. For the information part of an HRESULT, the range 0 to 512 (H0 to H200) is
reserved for system errors. The range 16382 to 16639 (H4000 to H40FF) is also reserved for OLE errors. When
specifying your own error codes, it is best practice to avoid these ranges entirely. You may also want to avoid the
codes used by ArcObjects.
More details on the ways in which you can declare, handle, and raise errors in your components are discussed
separately for the VB and VC++ environments.
Note on Error handling in the Extending ArcObjects sample projects
Error handling is included at its minimum throughout the examples in the book to keep the sample code as readable as
possible. If you are having problems with one of the VB samples, it is a simple matter to add error handling
throughout the component by using the ESRI Error Handler Generator. In the VC++ samples a minimum of HResult
checking is performed, which again is not a suitable pattern for released code, but helps keep sample code simpler to
read and understand.
VB and COM error handling
Some COM-compatible runtime environments check HRESULTS and automatically respond by translating the
information from the COM error object into the language-specific error handling model.
VB has its own form of exception handling that uses the intrinsic global Err object. This mechanism is unrelated to the
COM error object, but VB developers are still able to successfully work with errors raised from COM methods, thanks to
the Visual Basic Virtual Machine (VBVM).
The VBVM checks the value of every HRESULT returned from a COM method call. If the severity code of an HRESULT is
1 (indicating an error has occurred), the VBVM populates the Err object's Number property with the value of the
HRESULT. The data type of the VB global error object's Number property is a signed integer, and therefore, the
unsigned HRESULT integer is coerced to a signed integer, which results in all COM error codes in VB having a negative
value. The VBVM also attempts to populate the Source and Description fields with information retrieved from the COM
error object via IErrorInfo. Any HRESULT with a severity code of 0 is ignored by the VBVM; therefore, no error is
raised in VB, and VB does not receive any information from such codes.
Due to the 'under-the-covers' activity of the VBVM, VB users do not see HRESULTs returned from interface members in
VBinstead a member returns the parameter specified using the IDL attribute [out]. Another effect of this action is that
VB can only handle one error at a timeonly the most recent error can be discovered, although the underlying COM
error object may support a collection of errors.
You may also notice that the VBVM translates some COM errors into more VB-friendly termsfor example, the
E_NOTIMPL error (H80004001) becomes the VB error 445, 'Object doesn't support this action'. You can find the full list
of VB's trappable errors in the VB online reference or in MSDN.
Handling errors in your VB component
Using VB's On Error statement you can deal with any errors within your componentas a VB programmer you should
already be familiar with writing error handling routines.
To attempt to deal with the cause of an error, you must first work out which error you are dealing with. If you need to
identify the facility and information codes of an error number separately, you can use the functions below.
[Visual Basic 6]

Function FacilityCode(dword As Long) As Long


FacilityCode = ((dword And &HFFFF0000) \ &H10000) And &HFFF
End Function
Function InformationCode(ByVal e As Long) As Long
InformationCode = e And &HFFFF&
End Function
For example, the code excerpt below shows an error handler, which traps the error that a Geometry (m_pTopological)

75

is not Simple, and simplifies the geometry.


[Visual Basic 6]

ErrorHandler:
If (Err.Number < 0) And (Facility(Err.Number) = 4) Then
If BasicError(Err.Number) = esriGeometryError.E_GEOMETRY_NOTSIMPLE

Then

If Not (m_pTopological.IsSimple) Then


m_pTopological.Simplify
...
Else
...
At this point, your action should depend on your code. You may want to use the Resume keyword to attempt the
operation which caused the error.
Raising errors in your VB component
If your component cannot recover from the error and continue running, you should raise an error back to your client.
This can also be performed by using the VB Err object. Often you may raise an error directly. For example, in the code
below, the standard COM error is raised to indicate that a particular method has no implementation code. A constant is
declared to hold the HRESULT value, and the error is raised within the interface member.
[Visual Basic 6]

Const E_NOTIMPL = &H80004001


...
Private Property Get ICommand_HelpContextID() As Long
Err.Raise E_NOTIMPL
End Property
The VBVM takes care of mapping the VB error to a COM error object and HRESULT, so clients can access the error
information as they would any other COM error.
Of course, it may be more appropriate to translate an error into your own custom error, giving your client an error that
relates directly to your component.
It is good practice to advertise the errors that you may raise by declaring an enumeration of constants. VB provides
the vbObjectError constant to help you define error codesthis sets the severity code to 1, and the facility code to 4.
When you decide on your own range of error codes, don't forget to add 512 to your constant to ensure you avoid the
reserved range of information codes.
[Visual Basic 6]

Public Enum enumMyErrors


myErrorNoLicense = vbObjectError + 512 + 500
myErrorGeneralError = vbObjectError + 512 + 501
End Enum
When the underlying error occurs, you can report the error back to your client. If you use a standard VB error number,
you do not have to pass a description string; however, if you are raising a custom error, you should pass a useful
description of your error back to your client.
[Visual Basic 6]

ErrorHandler:
If Err.Number = esriGeodatabase.fdoError.FDO_E_SE_OUT_OF_LICENSES Then
Err.Raise myErrorNoLicense, "Component_Class", _
"You do not have a license to use that editing functionality."
End If
End If
End Sub
The ESRI ErrorHandler add-in for VB may be useful when you are writing and debugging your components. However,
the ErrorHandler relies on a UI component to handle errors, and therefore, should never be used if your component
may run in a server environment, and is not suitable for any deployed code.
COM error handling in VC++
In the VC++ environment, unlike VB, it is the responsibility of the COM client to check the HRESULT of each method
call to see if an error has been created. You can then interrogate the COM error object for information about the error.
Similarly, if an exception is generated in your code, it is your responsibility to create the COM error object and return
the appropriate HRESULT.
Handling errors in your VC++ component
You should explicitly check the return values of all method calls to uncover any errorsthis includes calls to property
accessors (get_ and put methods). Use the FAILED and SUCCEEDED macros defined in WinError.h.
[Visual C++]

hr = ipFeatureLayer->put_Visible(VARIANT_FALSE);
The simplest course of action when you find a failure is to exit the procedure, returning the same HRESULT.

76

[Visual C++]

if (FAILED(hr)) return hr;


However, if you want to investigate the error further and attempt to resolve it, you can use the GetErrorInfo function
defined in the Ole automation library.
[Visual C++]

if (FAILED(hr))
{
IErrorInfoPtr ipError;
::GetErrorInfo(0,&ipError);
ipError->GetDescription(&bError);
' Do something based on the error here.
}
Creating error information in your VC++ ATL component
In addition to returning an HRESULT, you can also create a COM error object to give additional information about an
exception. This step is not mandatory, although it is considered best practice.
Any class that creates the COM error object should also implement the additional COM interface ISupportErrorInfothis
indicates to clients that an error object may be created by this class and also ensures that error information can be
returned to the client.
A class that creates COM error objects using the IErrorInfo interface needs to implement the
ISupportErrorInfo interface.
When it comes to returning errors to clients of your component, the process is somewhat simplified if you create your
class using the ATL Object Wizard. In the ATL Object Wizard Properties dialog box, the Attributes tab contains an
option called 'Support ISupportErrorInfo'. Selecting this option will create a class that supports ISupportErrorInfo, and
a method will be added to your class that looks something like the code below.
[Visual C++]

STDMETHODIMP CMyClass::InterfaceSupportsErrorInfo(REFIID riid)


{
static const IID* arr[] =
{
&IID_IMyClass
};
for (int i=0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
if (InlineIsEqualGUID(*arr[i],riid))
return S_OK;
}
return S_FALSE;
}
Using the ATL Object Wizard simplifies the process of returning errors from your VC++ class.
You can define an enumeration of error codes in your IDL file, so that clients can find out which errors you may create.
You can use the MAKE_HRESULT macro, defined in the Winerror.h header file, to build your HRESULT.
typedef enum enumMyClassErrorCodes
{
[helpstring("My Class in invalid")]
E_MYCLASS_INVALID = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 0x3000)
} enumMyClassErrorCodes;
Remember that clients, such as VB, will not be able to retrieve any information if you return an HRESULT with a
severity code of 0 (indicating nonstandard success of a method call).
You can use the ATL global function AtlReportError to create and populate the COM error object.
[Visual C++]

AtlReportError(CLSID_MyClass, _T("No connection to Database."), IID_IMyClass, E_FAIL);


The function also has a form that works with resource files to ensure easy internationalization of the message strings.
[Visual C++]

AtlReportError(CLSID_MyClass, IDS_DBERROR, IID_IMyClass, E_FAIL, _Module.m_hInstResource);


If you return a failed HRESULT without specifying the details of the error in the COM error object, the string "Unknown
Error" will be used by clients such as VB.

77

Chapter 3: Extending the User Interface


Extending the Framework
Extending the User Interface
As an overview of the classes that you can use to extend the user interface, this topic serves as an introduction to the
following example.
About Extensions
General information about creating Extension classes and just-in-time (JIT) extensions.
Commands And Tools Example
The first example project in this section provides an example of how to create the basic building blocks of many
customizations. You may be aware that the creation of custom commands and tools is covered already in the ArcGIS
Desktop Developer Guide and ArcGIS Engine Developer Guide, and various samples exist for command and tool
classes in the Samples section of the ArcGIS developer help. However, commands, tools, extensions, and so on, are
used in many examples throughout Extending ArcObjects, and so a review of creating basic custom commands and
tools are also covered in this chapter.
In this example you can find the following customizations, which are part of an example extension class:

Creating a Button: Creating a basic button (or command) by implementing ICommand.

Creating a Subtyped Command: Creating a basic subtyped command (group of related commands) by
implementing ICommand and ICommandSubtype.

Creating a Tool: Creating a basic tool by implementing ITool and ICommand.

Creating a ToolControl: Creating a basic toolcontrol by implementing ICommand and IToolControl.

Creating a MultiItem: Creating a dynamic menu item, which appears as zero or more adjacent menu items on a
menu, by implementing IMultiItem and IMultiItemEx.

Creating a Menu: Creating a custom menu by implementing IMenuDef.

Creating a Toolbar: Creating a custom toolbar by implementing IToolbarDef.

Creating an Extension: Creating a an Extension by implementing IExtension and IExtensionConfig.

DDE Handler
DDE Handler Example
An example demonstrating how you can create a class to handle incoming DDE calls to ArcMap.

Extending the User Interface


The ArcGIS framework allows you to extend the framework by adding your own buttons, tools, and tool controls. To
create a COM-based command, you would create classes that implement certain interfaces. These classes are then
compiled into ActiveX DLLs that can plug into the ArcGIS applications. The term COM-based command is used here to
refer to the type of commands you create by implementing the interface and compiling into DLLs, instead of the types
of commands that are created using the built-in VBA environment, such as macros and UIControls.
The interfaces discussed in this section (ICommand, ITool, IToolControl, ICommandSubtype, IMultiItem, and
IMultiItemEx) are generally implemented to create custom commands. You can create the following types of
commands: Button, Tool, ToolControl, and MultiItem.
It is rare that you would use these interfaces to query the properties of the command. In the application, all
commands are exposed through command items (see the object model diagram and discussion of command classes
below), so you would use the ICommandItem interface to query the properties or to override some of the properties of
the underlying command.
You will rarely use the command interfaces to query existing commands; instead, you will generally
use them to create custom commands.
When you are distributing custom commands, it is not a requirement to package these commands in an extension. You
can simply deliver a DLL that contains only commands if your application doesn't require the extra functionality that an
extension can offer. You may want to additionally use an extension if you want a group of commands to be enabled or
not depending on whether an extension is turned on. This is useful if you have licensing built into your extension. You
might also want to use an extension if your commands share some common data or setting, or if you want to persist
some information in the document.

78

About the command classes

Buttons are simple commands that act as buttons or menu items and perform simple actions when clicked. Buttons
can be put on toolbars and menus. To create a custom button, you only have to implement ICommand.
A tool acts as a button that allows further interaction with the application display. The Zoom In tool is a good example
of a toolyou click or drag a rectangle over the map display to define the area on which to zoom. Tools can only be
put on toolbars. To create a custom Tool object, implement both ICommand and ITool.
A tool control is a dropdown list box control, editable text box control, or other type of control that can be added to a
toolbar. To create a custom ToolControl object, implement both ICommand and IToolControl. Be aware that only one
instance of a particular tool control is allowed to exist in the application at any one time.
A subtyped command object is a group of related commands that can share properties. Subtyped commands can be
put on toolbars and menus. The ICommandSubType interface is used when you want to have more than one
command, tool, or tool control in a single class. You would implement both ICommand and ICommandSubType (and
possibly ITool or IToolControl, depending on what type of command you are creating) in your class.
A MultiItem object is a dynamic command that appears as zero or more adjacent menu items on a menu, depending
on the state of the application. A MultiItem can be used when items on a menu cannot be determined prior to run
time, or the items need to be modified based on the state of the system. The menu items at the bottom of the File
menu, which represent the most recently used files, are a good example of this.

To create a custom toolbar to contain existing or custom commands and tools, you would implement the IToolbarDef
interface. Once this class is registered in one of the command bar component categories, the command bars collection
uses the definition of the toolbar in your class to create the actual command bar.
To create a custom menu, implement IMenuDef. The IMenuDef interface is identical to the IToolbarDef interface
except that it is used to indicate to the application that this is a menu.
If you are creating a root menu, that is, a menu that will appear in the Menus command category in the Customize
dialog box, implement both IMenuDef and IRootLevelMenu. IRootLevelMenu is an indicator interface that is only used
to indicate to the application that the menu should be treated as a root menu.
If you are creating a context menu, implement both IMenuDef and IShortcutMenu. IShortcutMenu is an indicator
interface that is only used to indicate to the application that this menu should be treated as a context menu.
See Also Extending the Framework, Commands and Tools Example.

79

Commands And Tools Example


Object Model Diagram

Example Code Click here.


Description This project provides an example of an extension complete with a command, menu, multi-item, subtyped
command, tool, toolbar, and tool control. This project is designed to show you how to use the different types of
command items and how to combine them into an extension.
Design Coclass SampleCommand and Coclass SampleSubtypedCmd are subtypes of Button, Coclass SampleTool is a
subtype of Tool, Coclass SampleToolControl is a subtype of ToolControl, Coclass SampleMultiItem is a subtype of
MultiItem, Coclass SampleMenu is a subtype of MenuDef, Coclass SampleToolbar is a subtype of ToolbarDef, and
Coclass SampleExtension is a subtype of Extension.
License required ArcView or above
Libraries ArcMapUI, Carto, Display, Framework, Geometry, System, and SystemUI.
Languages Visual Basic
Categories ESRI Mx Commands, ESRI Mx Command Bars, and ESRI Mx JIT Extensions.
Interfaces ICommand, ICommandSubtype, IExtension, IMenuDef, IMultiItem, IMultiItemEx, ITool, IToolbarDef, and
IToolControl
How to use
1.

Register the CommandsAndToolsVB.dll and double-click the CommandsAndToolsVB.reg file to register to


component categories.

2.

Open ArcMap and add a few data layers to the map.

3.

Click Tools, then click Customize.

4.

In the Customize dialog box, click the Toolbars tab and check Sample Toolbar. Click Close to dismiss the
Customize dialog box.

You should now see the Sample Toolbar in ArcMap.


5.

Again click Tools, then click Extensions.

80

6.

In the Extensions dialog box, check Sample Extension, then click Close to dismiss the Extensions dialog box.

The Sample Toolbar should now be enabledyou can use them to interact with the map.

Commands and Tools Example


In this example you will find a simple example of all the different types of commands, organized on custom menus and
toolbars. Also, all of these items work with an extension; the extension controls the enabled state of each command. If
the extension is turned on in the application, then the commands will be enabled; otherwise, the commands will be
disabled.
This example demonstrates a simple implementation of basic commands, tools, menus, a toolbar,
and an extension.
This sample adds commands and tools to the ArcMap application; however, the principles are similar for all ArcGIS
applications, although such customizations would involve registering to different component categories and, of course,
working with the application, document, and associated objects differently.

Creating the SampleCommand

A custom button is one of the most common and simple types of custom class you can create for the ArcGIS
framework. To create the SampleCommand, which is a type of button, create a new class and implement ICommand.
You will add code to the command to replicate the existing Full Extent command.
Implementing ICommand
The ICommand interface must be implemented by all COM-based commands (except for MultiItems), often in
conjunction with other interfaces. This interface determines the behavior and properties of simple commands, such as
buttons and menu items. For example, the ICommand interface sets command properties such as caption, name,
category, bitmap, status bar message, ToolTip, help context ID and help file, enabled state, and checked state. It also
defines what action happens when the command is clicked.
The OnCreate method occurs just after the command is instantiated and provides a hook to the application object that
instantiated the command. Once you have this reference to the application object, you can access the other objects in
the application. For the SampleCommand, check that the hook received is the expected type before storing a reference
to the ArcMap application and MxDocument objects. Also, find the SampleExtension object, and save a reference to
this. You will use this later in the Enabled property.
[Visual Basic 6]

Private m_pApp As esriFramework.IApplication


Private Sub ICommand_OnCreate(ByVal hook As Object)
If TypeOf hook Is esriArcMapUI.IMxApplication Then
Set m_pApp = hook
Set m_pMxDoc = m_pApp.Document
' Get the extension
Dim u As New esriSystem.UID
u.Value = "CmdToolbarExt.SampleExtension"
Set m_pExt = m_pApp.FindExtensionByCLSID(u)
End If
End Sub
It is important to always check to see what type of object is passed in by hook. For example, if your command is
designed to only work in ArcMap, you should check that the hook object implements IMxApplication before saving the
reference to the ArcMap Application object.
The type of hook received by a command may depend on how the command was instantiated and the application that
is hosting the command. The ArcMap application may instantiate any commands in the ESRI Mx Commands
component category, for example, clicking Tools, clicking Customize, then choosing a tool; in this case the hook
passed to your command will be a reference to the esriArcMap Application object. For more information, see the
'Command Component Categories' section below.
If you are working with ArcGIS Engine and intend your command to be used not just in ArcMap but also with the
ArcGIS MapControl, PageLayoutControl, and ToolbarControl, you may want to consider using the HookHelper. This
class can be used to hold the hook passed to OnCreate and will return the ActiveView, PageLayout, or FocusMap,
regardless of which hook class was received. You can find more information about using a HookHelper in the
esriControlCommands Library Reference section of ArcGIS Developer Help.

81

The Enabled property is used to specify in what state the application should be in for the command to be enabled. It is
important to minimize the amount of code that goes here because the system calls this property often. For the
SampleCommand, return True if the SampleExtension is enabled.
[Visual Basic 6]

Private Property Get ICommand_Enabled() As Boolean


If m_pExt.State = esriESEnabled Then
ICommand_Enabled = True
Else
ICommand_Enabled = False
End If
End Property
The OnClick method controls what action happens when this command is clicked by the user. For the
SampleCommand, add code to set the Extent of the active view to the full extent.
[Visual Basic 6]

Private Sub ICommand_OnClick()


' Zoom to the full extent
Dim pActiveView As esriCarto.IActiveView
Set pActiveView = m_pMxDoc.ActiveView
pActiveView.Extent = pActiveView.FullExtent
pActiveView.Refresh
End Sub
The Bitmap property is used to set the bitmap for the icon on this command. Commands are displayed by default
using only their caption, but can also be displayed using only the bitmap or using both bitmap and caption. You do not
always need to provide a bitmap for a commandif you do not return a bitmap, the command caption will be
displayed by default.

If you are working in VB, you may want to store the bitmap in a resource file or in a PictureBox control on a form. For
the SampleCommand, add a resource file to your project and add the bitmap to this resource file, then set the bitmap
during class initialization and return the bitmap.
[Visual Basic 6]

Private m_pBitmap As IPictureDisp

'Bitmap for the tool

Private Sub Class_Initialize()


Set m_pBitmap = LoadResPicture(104, vbResBitmap)
End Sub
Private Property Get ICommand_Bitmap() As esriSystem.OLE_HANDLE
ICommand_Bitmap = m_pBitmap
End Property
You can use the Visual Basic 6 Resource Editor (accessed from Project/Add new resource file) to create a resource file
and add resources to it. If you don't have the resource editor add-in loaded into Visual Basic, you can register
C:\Program Files\Microsoft Visual Studio\VB98\Wizards\RESEDIT.DLL and go to Add-Ins/Add-In Manager to enable
this add-in. This add-in is part of Visual Basic, but may not be enabled by default.
You must use a Bitmap file (.bmp) for the Bitmap property, as Icon files (.ico) are not supported. Bitmap files should
be 16 by 16 pixels. The color of the upper left pixel of the bitmap is treated as the transparent color. For example, if
the upper left pixel of the bitmap is red, then all of the red pixels in the bitmap will be converted to transparent.
The Checked property indicates the state of this command. If a command item appears pressed in on a command bar,
the command is checked. Commands that serve as toggles will be checked when that toggle is on. If no bitmap is set
for the command and the command is on a menu, a check mark will be displayed if the command is checked. By
default, Checked is set to False. In the SampleCommand, you can return True if the current extent is the full extent of
the data frame.
[Visual Basic 6]

Private Property Get ICommand_Checked() As Boolean


Dim pDisplayTransformation As esriDisplay.IDisplayTransformation
Set pDisplayTransformation = m_pMxDoc.ActiveView.ScreenDisplay.DisplayTransformation
Dim pCurExt As esriGeometry.IRelationalOperator
Set pCurExt = pDisplayTransformation.VisibleBounds
If pCurExt.Equals(pDisplayTransformation.Bounds) Then
ICommand_Checked = True
Else

82

ICommand_Checked = False
End If
End Property
The ICommand interface includes a number of string properties, which are used to help identify the command in the
user interface:

The Caption property of a command is the string that appears when the command is placed on a command bar if
the command's display type on the command bar is set to Text Only or Image and Text. The Caption is also the
text that you see for the command when the command is listed in the Commands list in the Customize dialog
box.
[Visual Basic 6]

Private Property Get ICommand_Caption() As String


ICommand_Caption = "SampleCommand (zoom to full extent)"
End Property

Category determines where the command will appear in the Commands panel of the Customize dialog box. This
is not to be confused with the component category. For the SampleCommand, return "Extending ArcObjects" as
the category.

The Name property of a command is a programmatic identifying name string. By convention, the Name property
should include its category name and caption, using a format similar to <Category>_<Caption>. For example,
the name of the built-in ArcMap command About ArcMap is "Help_About"; this format helps you find the
appropriate command ID in the ArcID module. Generally, the Name property does not include any spaces, for
programmatic simplicity.
[Visual Basic 6]

Private Property Get ICommand_Name() As String


ICommand_Name = "ExtendingArcObjects_SampleCommand"
End Property
If your application were to be translated into more than one language, you would translate the caption but not
the name. You would keep the name in the original language since that string may be used in your code or other
code to find or identify the command.

The Message property is used to set the message string that appears in the status bar of the application when
the mouse passes over the command.

The ToolTip property sets the string that appears in the screen tip when the mouse hovers over the command.

The HelpFile property is used to specify the name of the help file that contains the context-sensitive help topic for your
custom command. The HelpContextID property is used to specify the mapped numeric value of the topic ID of the
context-sensitive help topic for your custom command. If you set both the HelpFile and HelpContextID properties, then
an end user of your command can use the "What's This?" command in ArcMap or ArcCatalog to get help on your
command. Note, if you want your What's This help to look and behave the same as the standard ArcGIS commands,
then you should create a WinHelp file instead of a HTMLHelp file. As you have not implemented any help for your
SampleCommand, you can leave the implementation of these members blank, which will return an empty string.
You can find more information on providing help for your customization, including for a command or tool, in Chapter 2,
Implementing help for custom classes.
Register the SampleCommand to the ESRI Mx Commands component category.

83

Command Component Categories


As mentioned above, the hook object passed to command may vary, depending on how the command was
instantiated. Below is a list of the most common component categories which you may register commands to, and the
type of hook they may receive.
Component Category

Hook

ESRI Mx Commands

esriArcMap Application

ESRI Mx File Menu Commands

esriArcMap Application

ESRI Mx Tool Menu Commands

esriArcMap Application

ESRI EditTool Menu Commands

esriArcMap Application

ESRI Sketch Menu Commands

esriArcMap Application

ESRI SketchTool Menu Commands

esriArcMap Application

ESRI Gx <...> Commands

esriArcCatalog Application

ESRI GMx Commands

esriArcGlobe Application

ESRI Sx Commands

esriArcScene Application

Creating the SampleSubtypedCmd

You can create a custom subtyped command in a similar way to creating a custom command, the difference being that
you create one class, which can act as many different commands, instead of having to create and manage a number of
separate command classes. To create the SampleSubtypedCmd, create a new class and implement both ICommand
and ICommandSubtype. You will add code to your SampleSubtypedCmd to replicate the functionality of the existing
Previous View and Next View commands and provide in a single class one button to zoom to the previous extent and
another button to zoom to the next extent.
Implementing ICommandSubtype
When creating a subtyped command, it is generally easier to implement the straightforward ICommandSubtype
interface first. Work out how many commands you will be providing in the class, and return this value from GetCount.
[Visual Basic 6]

Private Function ICommandSubType_GetCount() As Long


ICommandSubType_GetCount = 2
End Function
This method can be used programmatically to work out how many subtypes are provided by the class; in this
particular example, this method is not called.
The most important member is SetSubType. A separate instance of the subtyped command is created for each
command item required, and its Subtype is set after instantiation. The subtype of any instance will remain throughout
the instance's lifetime, but will vary from instance to instance. Save the value, which is passed to SetSubType, as a
member variable so that you can complete the members of ICommand appropriately.
[Visual Basic 6]

Private Sub ICommandSubType_SetSubType(ByVal SubType As Long)


m_lSubType = SubType
End Sub
Implementing ICommand
Use a case statement in each property of ICommand to determine which subtype is being queried; apart from that,
complete the ICommand members as you would for any other command. For example, return a different caption
depending on the current subtype.
[Visual Basic 6]

Private Property Get ICommand_Caption() As String


Select Case m_lSubType
Case 1
ICommand_Caption = "SampleSubtypedCmd (zoom to previous)"
Case 2
ICommand_Caption = "SampleSubtypedCmd (zoom to next)"
End Select
End Property

84

Name should return the same name, regardless of subtype, as Name is used for programmatic identification. OnClick
can also be coded regardless of subtype, as either operation will require access to the MxDocument. Also, as for the
SampleCommand, store a reference to the SampleExtension, which you can use in the Enabled property.
[Visual Basic 6]

Private m_pApp As esriFramework.IApplication


Private m_pMxDoc As esriArcMapUI.IMxDocument
Private m_pExt As esriSystem.IExtensionConfig
...
Private Sub ICommand_OnCreate(ByVal hook As Object)
Set m_pApp = hook
Set m_pMxDoc = m_pApp.Document
Dim u As New esriSystem.UID
u.Value = "CmdToolbarExt.SampleExtension"
Set m_pExt = m_pApp.FindExtensionByCLSID(u)
End Sub
In OnClick, zoom to the next or previous extent, depending on the current subtype.
[Visual Basic 6]

Private Sub ICommand_OnClick()


Dim pExtentStack As esriCarto.IExtentStack
Set pExtentStack = m_pMxDoc.ActiveView.ExtentStack
Select Case m_lSubType
Case 1
If pExtentStack.CanUndo Then pExtentStack.Undo
Case 2
If pExtentStack.CanRedo Then pExtentStack.Redo
End Select
End Sub
As you can see from the code above, one advantage of creating similar commands as a subtyped command is that you
can share the basic code between the commands; above, both command subtypes share the same code to access the
ExtentStack.
In the sample project, a bitmap of each potential image is created, and the appropriate image returned from the
Bitmap property. This means that multiple bitmaps are loaded into memory but remain unused. As the subtype of the
instance is unknown at initialization, you could decide to create only the appropriate Bitmap when the Bitmap property
is first accessed.
Register the SampleSubtypedCmd to the ESRI Mx Commands component category.

Creating the SampleTool

A custom tool relies on user interaction with the application display. For example, the SampleTool replicates the
existing Zoom In tool, where a user clicks on the display and drags a rectangle over the map display to define the area
on which to zoom. To create the SampleTool, create a new class and implement both ICommand and ITool.
Implementing ICommand
You should implement ICommand for a Tool class in a similar way as you did for the SampleCommand; however, there
are a few behavioral differences to take account of when creating a tool.
The OnClick method for a command is usually used to perform the main function of your class, but for a Tool the
OnClick member will probably be used to initialize the tool. For the SampleTool, store references to the Application,
MxDocument, and SampleExtension as you did for the other command classes.
[Visual Basic 6]

Private m_pApp As esriFramework.IApplication


Private m_pMxDoc As esriArcMapUI.IMxDocument

85

Private m_pExt As esriSystem.IExtensionConfig


Private Sub ICommand_OnCreate(ByVal hook As Object)
Set m_pApp = hook
Set m_pMxDoc = m_pApp.Document
Dim u As New esriSystem.UID
u.Value = "CmdToolbarExt.SampleExtension"
Set m_pExt = m_pApp.FindExtensionByCLSID(u)
End Sub
The tool that is currently active will appear checked; however, unlike ordinary commands, this state is not controlled
by the ICommand Checked property. The ArcGIS framework automatically manages the appearance of the active tool
on the toolbar; there is nothing your code needs to do for this. Unlike a command, you should ensure you always
provide a bitmap for a tool, as tools are displayed by default using their bitmap only.
The remaining membersCaption, Category, Enabled, HelpContextID, HelpFile, Message, and Namecan be
implemented like any standard command.
Implementing ITool
The ITool interface differentiates a tool from a command and allows a tool to interact with the application's display.
Only one tool can be active in the application at a time. Using the members of ITool you can define what occurs on
events, such as mouse move, mouse button press and release, keyboard key press and release, double-click, and
right-click, when your tool is active. To add the zoom in functionality to your SampleTool, add code to the MouseDown
method to allow the user to track a rectangle that defines the new extent of the view.

[Visual Basic 6]

Private Sub ITool_OnMouseDown(ByVal Button As Long, ByVal Shift As Long, ByVal X As Long, ByVal Y As
Long)
If Button = 1 Then
Dim pActiveView As esriCarto.IActiveView
Set pActiveView = m_pMxDoc.FocusMap
Dim pRubberBand As esriDisplay.IRubberBand
Set pRubberBand = New esriDisplay.RubberEnvelope
pActiveView.Extent = pRubberBand.TrackNew(pActiveView.ScreenDisplay, Nothing)
pActiveView.Refresh
End If
End Sub
Using IRubberBand objects in this manner may block the message queue to your tool, which may lead to unexpected
consequences. For example, the TrackNew method of the code above may block messages to the OnMouseUp or
OnContextMenu methods of the tool. If you handle only a left mouse click in OnMouseDown; however, the OnMouseUp
and OnContextMenu methods will be called as expected if the right mouse button is clicked and released.
Use the Cursor property to set the mouse pointer of the tool. For the SampleTool, add a cursor file (.cur) to the
resource file in the project and use this.
[Visual Basic 6]

Private m_pCursor As IPictureDisp

'Cursor for the tool

Private Sub Class_Initialize()


...
Set m_pCursor = LoadResPicture(101, vbResCursor)
End Sub
Private Property Get ITool_Cursor() As esriSystem.OLE_HANDLE
ITool_Cursor = m_pCursor
End Property
Add some logic to the Deactivate method to specify whether this tool can be deactivated. The default Deactivate value

86

for a tool is False, which means that the tool cannot be interrupted by another tool. The Deactivate method will be
called when the tool is active and the user selects another tool instead of using the active tool. If the tool returns
False, the application will not allow the other tool to become active.
You may want to return False from Deactivate if your tool performs some setup actions in OnClick that leave the
application in an 'unfinished' state, which can only be completed by using the tool events. Generally, tools should
return True when they perform a simple one-step operation and do not change the state of the application beyond
their main function. As the SampleTool simply performs a one-step operation in OnMouseDown, you can return True
from the SampleTool's Deactivate method to allow users to select another tool instead.
[Visual Basic 6]

Private Function ITool_Deactivate() As Boolean


ITool_Deactivate = True
End Function
You can write code to display a custom context menu when the right mouse button is pressed and the tool is active by
adding code to the OnContextMenu method. The sample tool displays a context menu in OnContextMenu containing
the Zoom to Next and Zoom to Previous extent command subtypes.
[Visual Basic 6]

Dim pShortCut As ICommandBar


Set pShortCut = m_pApp.Document.CommandBars.Create("MyShortCut", esriCmdBarTypeShortcutMenu)
Dim u As New esriSystem.UID
u.Value = "CmdToolbarExt.SampleSubtypedCmd"
u.SubType = 1
pShortCut.Add u, 0
u.Value = "CmdToolbarExt.SampleSubtypedCmd"
u.SubType = 2
pShortCut.Add u, 1

pShortCut.Popup
If your tool displays a custom context menu, it should let the application know that it handled the OnContextMenu
event by returning True from the OnContext function. If you don't do this, the standard context menu will be displayed
after your custom context menu. This may be useful for ensuring that the standard context menu is displayed if the
mouse is clicked somewhere on the view for which it is not appropriate to display your custom context menu. If you do
not want to provide a context menu, but do not want to display the standard context menu either, you can simply
return True from OnContextMenu.
The other On.. methods of ITool are not required by the SampleTool. However, if you do provide implementation code
for the MouseMove method, take carethis will be called frequently, when the user moves the mouse over the map
while the tool is active. Do not place time-consuming code here, and never show message boxes or dialog boxes here.
Use the OnKeyDown method to capture keyboard strokes from the user while the tool is activeyou may, for
example, want to allow a user to complete the use of a tool by typing in some text (similar to the New Text Tool in
ArcMap).
Register the SampleTool to the ESRI Mx Commands component category.

Creating the SampleToolControl


A ToolControl provides a more complex item for a toolbar. Essentially, a ToolControl passes a window handle back to
the application, allowing you to add (within reason) any type of window to the toolbar, instead of simply displaying a
button. Examples of ToolControls are Map Scale on the Standard toolbar, Font and FontSize on the Draw toolbar, and
Task and Target on the Editor toolbar. As you can see below, many of the existing ToolControls provide some kind of
dropdown listthis sort of control is ideal as the basic control can fit onto the height of the toolbar. When the control
is clicked, the full list will be displayed beyond the toolbar.

87

To create the SampleToolControl, create a new class, and implement ICommand and IToolControl. You will add code to
the SampleToolControl to zoom to any layer in the active view by selecting the layer from a list.
Implementing ICommand
For a toolcontrol, you would implement the ICommand interface the same a way as you did for the simple button.
There are a few behavioral differences though, which are detailed below.
A user cannot change the display style (Image Only, Image and Text, or Text Only) of a tool control. A tool control
appears on a horizontally docked toolbar without a caption or bitmap; if the toolbar has vertically docked the tool
control, it will be represented by a button, which when pressed displays the tool control on a separate floating toolbar.

The Caption and Bitmap properties will appear in the Commands list in the Customize dialog box. The bitmap will also
appear as the button on a vertically docked toolbar. You should always set the Bitmap property; otherwise, your tool
control will be displayed as a blank space on a vertical toolbar.
[Visual Basic 6]

Private m_pBitmap As IPictureDisp

'Bitmap for the toolcontrol

Private Sub Class_Initialize()


Set m_pBitmap = LoadResPicture(105, vbResBitmap)
End Sub
Private Property Get ICommand_Bitmap() As esriSystem.OLE_HANDLE
ICommand_Bitmap = m_pBitmap
End Property
In OnCreate, store references to the application, map document, and SampleExtension as you have done for the other
sample command items.
[Visual Basic 6]

Private m_pApp As esriFramework.IApplication


Private m_pMxDoc As esriArcMapUI.IMxDocument
Private m_pExt As esriSystem.IExtensionConfig
Private Sub ICommand_OnCreate(ByVal hook As Object)
Set m_pApp = hook
Set m_pMxDoc = m_pApp.Document
Dim u As New esriSystem.UID
u.Value = "CmdToolbarExt.SampleExtension"
Set m_pExt = m_pApp.FindExtensionByCLSID(u)
End Sub
The Checked property of a tool control will not be called.
Implementing IToolControl
The OnDrop method is used to specify on which type of command bar this tool control can be put. In most cases, tool
controls can only be used on toolbars.

88

[Visual Basic 6]

Private Function IToolControl_OnDrop(ByVal barType As esriSystemUI.esriCmdBarType) As Boolean


If barType = esriCmdBarTypeToolbar Then
IToolControl_OnDrop = True
End If
End Function
A tool control passes a window handle to the application via its IToolControl::hWnd property. For example, if you want
your tool control to be a simple ComboBox control, create a ComboBox control and pass the window handle of the
ComboBox control as the HWnd property. For the SampleToolControl, you need to display a list of layers in the map;
therefore, a ComboBox is a suitable control to use. However, it would be ideal to display a label next to the ComboBox
to give more information to the user. Therefore, add a form to your project and place a PictureBox on the form. On the
PictureBox, place a Label and a ComboBox (add the Label and ComboBox to the PictureBox, not the form, by ensuring
PictureBox is the selected item when you add the new controls). Return the window handle of the PictureBox from
IToolControl::hWnd.
[Visual Basic 6]

Private Property Get IToolControl_hWnd() As esriSystem.OLE_HANDLE


IToolControl_hWnd = frmControls.picToolControl.hWnd
End Property
You need to add a list of all the layers in the map to the ComboBox; add a public function to the form to do this.
[Visual Basic 6]

Public Sub UpdateLayerList()


...
frmControls.cboLayers.Clear
Dim pLayer As esriCarto.ILayer, i As Integer
For i = 0 To pMap.LayerCount - 1
Set pLayer = pMap.Layer(i)
frmControls.cboLayers.AddItem pLayer.Name
Next
...
End Sub

A tool control may remain the active item while a user adds a new layer, activates a different map frame, or changes
views. If this happens, you will need to update the list of layers in your tool control. Also, if a user opens a new
document, you will need to ensure all references to the document and map are updated. To achieve this, sink the
following events in the SampleToolControl:

NewDocument and OpenDocument events of the IDocumentEvents interface of the current document.

FocusMapChanged event of the IActiveViewEvents interface of the PageLayout.

ContentsChanged, ItemAdded, and ItemDeleted events of the IMapEvents interface of the Focus Map.

You can see how the references are set and kept up-to-date by looking at the code in the sample projectsee the
StartListeners method on the SampleToolControl.
The ComboBox now contains a list of the layers that are currently in the map. You now need to add code to zoom the
map to the extent of the selected layer when the ComboBox is used. This process begins with the
IToolControl::OnFocus method, which passes in an ICompletionNotify reference as a parameter. When your tool
control completes its actions, you must inform the application of this by calling the Complete method on this interface.
For the SampleToolControl, follow the steps below.
1.

Add a property to the form, allowing you to pass the ICompletionNotify reference to the form.
[Visual Basic 6]

Private m_pCompNotify As esriSystemUI.ICompletionNotify


...
Public Property Set CompNotify(pCompNotify As esriSystemUI.ICompletionNotify)
Set m_pCompNotify = pCompNotify
End Property
2.

In the IToolControl::OnFocus method, pass the ICompletionNotify reference to the form.

89

[Visual Basic 6]

Private Sub IToolControl_OnFocus(ByVal complete As esriSystemUI.ICompletionNotify)


Set frmControls.CompNotify = complete
End Sub
3.

Now, add code to the Click event of the ComboBox to identify the selected layer and zoom to its extent.
[Visual Basic 6]

Private Sub cboLayers_Click()


Dim pMap As esriCarto.IMap, selLayerName As String
Set pMap = m_pMxDoc.FocusMap
selLayerName = frmControls.cboLayers.Text
Dim pLayer As esriCarto.ILayer, i As Integer
For i = 0 To pMap.LayerCount - 1
If pMap.Layer(i).Name = selLayerName Then
Set pLayer = pMap.Layer(i)
End If
Next
Dim pEnv As esriGeometry.IEnvelope
Set pEnv = pLayer.AreaOfInterest
m_pMxDoc.ActiveView.Extent = pEnv
m_pMxDoc.ActiveView.Refresh
...
4.

The ComboBox control should lose focus after a user selects an item in the combobox, so finish the ComboBox
Click event by calling ICompletionNotify::SetComplete.
[Visual Basic 6]

...
If Not m_pCompNotify Is Nothing Then m_pCompNotify.SetComplete
End Sub
Now your SampleToolControl is almost complete. However, if you display the control as it is, the controls that form the
SampleToolControl will always appear to be enabled to the user. This is because when a tool control is disabled, the
command to disable is sent to the tool control window handle (for example, the PictureBox) and not to the controls it
contains (for example, the ComboBox and Label displayed). Therefore, add code to the ICommand::Enabled property
to set the enabled state of the Label and ComboBox when the tool control itself is disabled.
[Visual Basic 6]

Private Property Get ICommand_Enabled() As Boolean


Set m_pMap = m_pMxDoc.FocusMap
If m_pExt.State = esriESEnabled And m_pMap.LayerCount > 0 Then
ICommand_Enabled = True
frmControls.lblLayers.Enabled = True
frmControls.cboLayers.Enabled = True
Else
ICommand_Enabled = False
frmControls.lblLayers.Enabled = False
frmControls.cboLayers.Enabled = False
End If
End Property
About Controls for a ToolControl
Note, if you are using a Frame or PictureBox control as your window handle control for IToolControl, then you must set
the Visual Basic ClipControls property of the Frame or PictureBox to False. You might also want to use the following
settings for your frame or picturebox in VB for the control to look best on a toolbar.
Property

Value

Appearance

Flat

BackColor

MenuBar

BorderStyle

None

ClipControls

False

Be careful when sizing your tool control windowensure that the entire window area fits onto the restricted height of
the toolbar when displayed both in a horizontally and vertically docked toolbar.
Register the SampleToolControl to the ESRI Mx Commands component category.

90

Creating the SampleMultiItem


A MultiItem object is a dynamic command that appears as zero or more adjacent menu items on a menu, depending
on the state of the application. A MultiItem can be used when items on a menu cannot be determined prior to run time
or the items need to be modified based on the state of the system. The menu items at the bottom of the File menu,
which represent the most recently used files, are a good example of this.

The SampleMultiItem creates multiple menu items, each one corresponding to a layer in the map; when clicked, the
menu item will zoom in to that layer. Since the number of layers in the map is dynamic, a MultiItem is a good way to
implement this. To create the SampleMultiItem, create a new class and implement both IMultiItem and IMultiItemEx.
You do not implement the ICommand interface when creating a MultiItem. The SampleMultiItem will also work in
conjunction with the SampleToolControl you created previouslyit will update the SampleToolControl when one of the
items in the MultiItem is invoked so that it shows which layer was used to determine the extent of the view.
Implementing IMultiItem
By implementing IMultiItem, a single class can act like several adjacent menu items. During run time, the framework
notifies MultiItems when their host menu is about to be shown and how each subitem should appear.
Implement the Caption, HelpContextID, HelpFile, Message, and Name properties as you would for any command. From
the Caption property, return a string that you want to be displayed as the main command item.
The main concept to understand about implementing IMultiItem is the OnPopup method. This method occurs just
before the menu containing the MultiItem is displayed and provides two important parts of functionality. First, it
provides the hook to the application object that instantiated the MultiItem, and second it should return the number of
items in the MultiItem. Before the MultiItem is displayed, all the CommandItems that contain a MultiItem can query
the MultiItem OnPopup method to determine how many subitems should be displayed at that point. As your
SampleMultiItem will contain an item corresponding to each layer in the map, you can return the map layer count as
the number of items.
[Visual Basic 6]

Private m_pApp As esriFramework.IApplication


Private m_pMxDoc As esriArcMapUI.IMxDocument

'ArcMap application
'ArcMap document

Private m_pExt As esriSystem.IExtensionConfig


Private m_pMap As esriCarto.IMap

'Current focus map

Private Function IMultiItem_OnPopup(ByVal hook As Object) As Long


Set m_pApp = hook
Set m_pMxDoc = m_pApp.Document
Dim u As New esriSystem.UID
u.Value = "CmdToolbarExt.SampleExtension"
Set m_pExt = m_pApp.FindExtensionByCLSID(u)
' Set the number of items in the multiItiem to the number of layers.
Set m_pMap = m_pMxDoc.FocusMap
m_pLayerCnt = m_pMap.LayerCount
IMultiItem_OnPopup = m_pLayerCnt
End Function
After the OnPopup method has been called, the application will create a CommandItem for each subitem, and the
ItemBitmap, ItemCaption, ItemChecked, and ItemEnabled indexed properties will then be used to determine the
details of each sub-item. For these properties, the index value passed in represents the subitem number; from these
properties, return an appropriate value for each subitem. For example, the SampleMultiItem should indicate for each
ItemCaption which layer will be zoomed to.
[Visual Basic 6]

Private Property Get IMultiItem_ItemCaption(ByVal index As Long) As String


&nbspIMultiItem_ItemCaption = "Zoom to " & m_pMap.Layer(index).Name
End Property
Enable the different subitems if the SampleExtenstion is enabled and the corresponding layer is visible.
[Visual Basic 6]

Private Property Get IMultiItem_ItemEnabled(ByVal index As Long) As Boolean

91

If Not m_pExt.State = esriESEnabled Then


IMultiItem_ItemEnabled = False
Else
If m_pMap.Layer(index).Visible Then
IMultiItem_ItemEnabled = True
End If
End If
End Property
Finally, after the MultiItem has been displayed, the user will select an item, which will call the OnItemClick method,
again passing in the appropriate subitem number. In this method, find the extent of the selected layer and set the
extent of the map. Finally, you can also update the selected item in the SampleToolControl ComboBox to indicate
which layer was zoomed to.
[Visual Basic 6]

Private Sub IMultiItem_OnItemClick(ByVal index As Long)


' Get the layer, and the spatial-referenced extent of the layer.
Dim pLayer As esriCarto.ILayer
Set pLayer = m_pMap.Layer(index)
Dim pEnv As esriGeometry.IEnvelope
Set pEnv = pLayer.AreaOfInterest
' Zoom to the extent and refresh the view.
m_pMxDoc.ActiveView.Extent = pEnv
m_pMxDoc.ActiveView.Refresh
frmControls.cboLayers.ListIndex = index
End Sub
Implementing IMultiItemEx
IMultItemEx is an optional interface for MultiItems, which offers some additional indexed properties. By implementing
this interface you can have a separate message, help file, and help context id for each item in the MultiItem. For
example, implement the ItemMessage property as shown.
[Visual Basic 6]

Private Property Get IMultiItemEx_ItemMessage(ByVal index As Long) As String


IMultiItemEx_ItemMessage = "Zooms to " & m_pMap.Layer(index).Name & " layer."
End Property
The toolcontrol also works with the other commands in the extension. If another command causes the dataframe to
zoom into a layer, the toolcontrol is updated to reflect this change. For example, the sample multiItem updates the
toolcontrol when one of the items in the multiItem is invoked so that the toolcontrol always shows which layer is being
used to determine the extent of the view.
MultiItems are never added to the commands list in the Customize dialog box; they can only appear on menus.
Therefore, you need to create a custom menu to provide access to the SampleMultiItem; see the Creating the
SampleMenu section below.
Register the SampleMultiItem to the ESRI Mx Commands component category.

Creating the SampleMenu


A custom menu is straightforward to createthe only required interface is IMenuDef. The IMenuDef interface is similar
to the IToolbarDef interface (see below); its usage by the application is different because it is used to indicate to the
application that the class defines a menu.

The SampleMenu class defines a simple root-level menu that contains the SampleMultiItem command. To create the
SampleMenu, create a new class and implement IMenuDef and IRootLevelMenu.
Implementing IMenuDef
The IMenuDef interface is used to define the properties of a custom menu. Implement Caption and Name as you would
for the Caption and Name of ICommandsimply return strings to help identify the menu. (Caption is the string that
appears on the menu button in the user interface, Name is a programmatic identifying name string.)

92

[Visual Basic 6]

Private Property Get IMenuDef_Caption() As String


IMenuDef_Caption = "Zoom Menu"
End Property
Private Property Get IMenuDef_Name() As String
IMenuDef_Name = "SampleMenu"
End Property
From the ItemCount property, return how many items will appear on the menu. For the SampleMenu this will be one
item, the SampleMultiItem.
[Visual Basic 6]

Private Property Get IMenuDef_ItemCount() As Long


IMenuDef_ItemCount = 1
End Property
The IItemDef interface defines a command item on a menu; this interface is used in conjunction with the GetItemInfo
method on either the IToolbarDef or the IMenuDef interface to define the items on the toolbar or menu. This interface
specifies the identifier (CLSID or ProgID) of the command and also its subtype if applicable (if the command supports
ICommandSubtype).
To implement GetItemInfo for the SampleMenu, set the passed in IItemDef reference to the ID of the
SampleMultiItemthis is the only item that will be present on the SampleMenu.
[Visual Basic 6]

Private Sub IMenuDef_GetItemInfo(ByVal pos As Long, ByVal itemDef As esriSystemUI.IItemDef)


Select Case pos
Case 0
itemDef.ID = "CmdToolbarExt.SampleMultiItem"
End Select
End Sub

As the SampleMenu only has a single member, it is not essential to include the Select ... Case statement in this
member however, this structure is useful where you may add items to the menu in future versions and is also used
in the SampleToolbar code.
Implementing IRootLevelMenu
If you are creating a root menu, a menu that will appear in the Menus command category in the Customize dialog box,
implement both IMenuDef and IRootLevelMenu. IRootLevelMenu is an indicator interface that is only used to indicate
to the application that the menu should be treated as a root menu; it has no members. Implement IRootLevelMenu in
your SampleMenu.
Implementing IShortcutMenu
If you are creating a context menu, implement both IMenuDef and IShortcutMenu. IShortcutMenu is an indicator
interface that is only used to indicate to the application that this menu should be treated as a context menu; it has no
members. SampleMenu does not need to implement IShortcutMenu.
Register the SampleMenu to the ESRI Mx CommandBars component category.

Creating the SampleToolbar

At this point, your users will be able to use the commands and tools you have created by using the Customize dialog
box to add them to a menu or toolbar. It would be more convenient if you were to provide a custom toolbar which
contains all the items you have created so far. To create the SampleToolbar, create a new class and implement
IToolbarDef.

93

Implementing IToolbarDef
The IToolbarDef interface is used to define the properties of a custom toolbar. Its members are the same as IMenuDef,
which is described above in the SampleMenu example. With this interface you can determine the Caption and Name of
the toolbar and specify the command items which should appear on the toolbar.
The Caption property of a toolbar is the string that appears as the toolbar title when the toolbar is in a floating state.
Similar to the ICommand::Name property, the IToolbarDef::Name property is a programmatic identifying name string.
If your application were to be translated into more than one language, you would translate the Caption but not the
Name. You would keep the Name in the original language since that string may be used in your code to find or identify
the toolbar.
[Visual Basic 6]

Private Property Get IToolBarDef_Caption() As String


IToolBarDef_Caption = "Sample Toolbar"
End Property
Private Property Get IToolBarDef_Name() As String
IToolBarDef_Name = "Sample Toolbar"
End Property
From the ItemCount property, return how many items will be on this toolbarthe SampleToolbar will contain the
SampleCommand, SampleTool, SampleToolControl, each of the two SampleSubtypedCmd subtypes, and the
SampleMenu.
[Visual Basic 6]

Private Property Get IToolBarDef_ItemCount() As Long


IToolBarDef_ItemCount = 6
End Property
As described above for the IMenuDef interface, the GetItemInfo method should define the items on the toolbar.
[Visual Basic 6]

Private Sub IToolBarDef_GetItemInfo(ByVal pos As Long, ByVal itemDef As esriSystemUI.IItemDef)


Select Case pos
Case 0
itemDef.ID = "CmdToolbarExt.SampleMenu"
Case 1
itemDef.ID = "CmdToolbarExt.SampleToolControl"
Case 2
itemDef.ID = "CmdToolbarExt.SampleCommand"
itemDef.Group = True
Case 3
itemDef.ID = "CmdToolbarExt.SampleSubtypedCmd"
itemDef.SubType = 1
Case 4
itemDef.ID = "CmdToolbarExt.SampleSubtypedCmd"
itemDef.SubType = 2
Case 5
itemDef.ID = "CmdToolbarExt.SampleTool"
End Select
End Sub

Premier Toolbars
When an end user installs your custom toolbar, you may want this toolbar immediately available in the application so
that the user doesn't have to manually display that toolbar before using it. You can add a registry setting to make this
toolbar automatically appear the first time the application is run after the installation of your toolbar.
In the setup program for your toolbar, create a new key under:
HKEY_CURRENT_USER\Software\ESRI\ArcMap\Settings\PremierToolbars
The key name should be the CLSID of the toolbar. You don't have to set a value for this key. If you are working in
Visual Basic, you can use the ESRI Compile and Register Add-in to set up this registry key by clicking the Premier
Toolbars button and checking the box adjacent to SampleToolbar.

94

The PremierToolbars setting is only used the first time the application is started; if the user subsequently hides the
toolbar, no further attempts will be made to show the toolbar on application startup. After the application is started,
the value of your PremierToolbars key is set to 1 and is then ignored by the application.
Register the SampleToolbar to the ESRI Mx CommandBars component category.

Creating the SampleExtension

Throughout all the commands and tools in this example, an extension has been used to help maintain the state of the
individual items, tying the items together into a single entity. To create the SampleExtension, create a new class, and
implement IExtension and IExtensionConfig.
The SampleExtension will simply deliver the commands and tools and their associated toolbars and menus. The
SampleExtension controls the enabled state of each control. If the extension is turned on in the application, then the
controls will be enabled; otherwise, the controls will be disabled. Register the SampleExtension to the ESRI Mx JIT
Extensions component categoryfor more information about extensions and JIT extensions, see the About Extensions
topic.
Implementing IExtension
This interface allows you to set the name of the extension and specify what action takes place when the extension is
started or shut down. Use the Name property to set a Name string for this extensionthis will be a programmatic
string, which can be used to identify your extension. When IExtensionManager::FindExtensionByName is called, the
Name property will be used to identify the extension. This will not be the name displayed in the Extensions dialog box;
see the IExtensionConfig interface for more information.
[Visual Basic 6]

Private Property Get IExtension_Name() As String


IExtension_Name = "SampleExtension"
End Property
The Startup method is used to perform some action when the extension gets loaded. This method has a parameter
called initializationData, which is a reference to the object with which this extension is registered. For example, if the
extension is loaded in ArcMap, then the object type passed in by initializationData will be esriArcMap.Application. The
Shutdown method is used to perform some action when the extension gets unloaded. Again, see the About Extensions
topic for more information on startup and shutdown of extensions.
The sample extension does not need to make use of the Startup and Shutdown methods.
Implementing IExtensionConfig
For an extension to be listed in the Extensions dialog box, you need to implement the IExtensionConfig interface. The
Extensions dialog box in the ArcGIS applications allows users to turn extensions on and off manually. The
IExtensionConfig interface is used to provide the Extension dialog box with the name and description of the extension
and specifies the state of the extension.
The ProductName property specifies the name that is displayed for this extension in the Extensions dialog box. Use the
Description property to set the text that is displayed for this extension in the About this extension box in the

95

Extensions dialog box.


[Visual Basic 6]

Private Property Get IExtensionConfig_Description() As String


IExtensionConfig_Description = "This is a sample extension that controls the state of a command."
End Property
Private Property Get IExtensionConfig_ProductName() As String
IExtensionConfig_ProductName = "Sample Extension"
End Property
In the State property, use the incoming ExtensionState parameter (esriExtensionState enumeration) to determine
whether your extension should be enabled, disabled, or unavailable. When the state is enabled, the extension will be
displayed as checked in the Extensions dialog box. The checked state of the extension is saved in the user settings in
the registry.
Add a member variable, m_extState, to the SampleExtension to store the current state value. Set this variable to the
value of the ExtensionState in the State property Let, and return its value from the State property Get.
[Visual Basic 6]

Private m_extState As esriSystem.esriExtensionState


Private Property Get IExtensionConfig_State() As esriSystem.esriExtensionState
IExtensionConfig_State = m_extState
End Property
Private Property Let IExtensionConfig_State(ByVal ExtensionState As esriSystem.esriExtensionState)
m_extState = ExtensionState
End Property
IExtensionAccelerators is an optional extension interface, which you can implement to define keyboard accelerators for
your extension. If you do implement this interface, ensure that you do not change the behavior of existing accelerator
keys.
IAutoExtension is another optional interface for an extension, which can be implemented to control the state of the
extension by some method other than user intervention. If you return State esriESUnavailable from IExtensionConfig,
then the extension will be indicated as unavailable in the Extensions dialog box. If the user attempts to disable or
enable the extension, the application will inform the user that this is not possible for this type of extension. If you
return esriESEnabled or esriESDisabled, the Extensions dialog box will indicate the state, and use an unavailable box
to indicate it is an auto- enabling extension. IAutoExtension is an indicator interface with no members.
Compile the project and register the SampleExtension to the ESRI Mx JIT Extensions component category to
implement this as a just-in-time extension. Alternatively, you could use the ESRI Mx Extensions component category.
Now you can use the ArcMap Extensions dialog box to enable and disable the SampleExtension, which will control the
state of the SampleToolbar.
After compiling your project and registering the classes to the appropriate component categories, you should be ready
to use the toolbar in ArcMap (see the example for step by step instructions).

96

See also Extending the User Interface, About Extensions, and Extending the Framework.

About Extensions
An extension provides another mechanism for extending an application. An extension is a suitable choice of
customization if you want to deliver a package of associated functionality. Using an extension, you can provide many
different types of functionality, such as deliver controls and toolbars, perform event handling, store data commonly
shared between controls, perform validation, and much more.
Extensions can act as a central point of reference for developers when they are creating commands and tools for use
within the applications. Often these commands and tools must share data or access common UI components. An
extension is the logical place to store this data and develop the UI components. The main reason for this is that there
is only ever one instance of an extension per running application and, given an IApplication interface, it is always
possible to locate the extension and work with it.
An extension provides a mechanism for a developer to add a unit of additional functionality to an
ArcGIS application.
The Application object implements the IExtensionManager interface, which has properties to get a reference to a
particular extension and to get a count of how many extensions are currently loaded. To access the application
extension manager, QI for IExtensionManager on Application. Note that other types of objects can also implement
IExtensionManager. For example, the Editor toolbar in ArcMap is an extension that manages editor extensions.
Therefore, the Editor object also implements IExtensionManager. There is also an ExtensionManager object that
implements IExtensionManager.
Extension classes registered to the following component categories will be started up when the appropriate ArcGIS
application is started.

ArcMap extensions are loaded from 'ESRI Mx Extensions'.

ArcCatalog extensions are loaded from 'ESRI GX Extensions'.

ArcScene extensions are loaded from 'ESRI SX Extensions'.

ArcGlobe extensions are loaded from 'ESRI GMx Extensions'.

Just-in-time Extensions
Extensions are generally loaded when an ArcGIS application first starts up. For example, in ArcMap the extension
manager instantiates each extension it can find in the ESRI Mx Extensions component category and calls its
IExtension::Startup method.
You may want to develop your extension to be a just-in-time (JIT) extension. The concept behind JIT extensions is to
avoid creating objects until necessary; therefore, ArcMap does not start up a JIT extension until it is actually required.
JIT extensions are like standard extensions; however, they are not started up at application startup,
but only when they are required.
To create an ArcMap JIT extension, you would create your extension, like a standard extension, by implementing the
extension interface(s) as required. Then you just have to register the extension with the ESRI Mx JIT Extensions
component category, and the application framework takes care of the rest. The JIT extension manager in ArcMap is
essentially similar to the extension manager, but maintains the list of both JIT extensions and also maintains
information on whether each is currently started up or nota JIT extension will be started up the first time that
IApplication::FindExtensionByCLSID is called for that extension.

97

For example, if your extension delivers a number of command items on a toolbar, like the commands and tools
example in this chapter, you can design your extension so that the visibility of the toolbar controls the point at which
the extension is loaded (started up). In this case, when the toolbar is made visible in ArcMap (by selecting the toolbar
in the View, Toolbars menu), the command items on the toolbar are instantiated. It is these command items which
control the extension startup - this is because members such as IMultiItem::OnPopup and ICommand::OnCreate call
the FindExtensionByCLSID method to get a reference to the extension that they belong to. When
FindExtensionByCLSID is called, the JIT extension manager recognizes that the extension in question is not already
started up, and will then call its Startup method. If the toolbar is closed, the extension will not be loaded the next time
that application is started, until the toolbar is once again made visible.
If you are using the IExtensionManager interface to iterate extensions, it will not list any JIT extensions. Similarly, if
you are using IJITExtensionManager, ordinary extensions will not be listed. Keep in mind that iterating all the JIT
extensions by calling FindExtensionByCLSID will start up each JIT extension at that point, leaving them in this state
until ArcMap exits.
Here are a few things that you need to keep in mind when creating a JIT extension:

Register to ESRI Mx JIT Extensions for an ArcMap JIT extension, (or ESRI GX JIT Extensions for ArcCatalog, ESRI
GMx JIT Extensions for ArcGlobe, or ESRI SX JIT Extensions for ArcScene). Remove the registration to the
existing extension component category.

You need to be careful if your extension listens for document events (IDocumentEvents, IDocumentEventsDisp,
and so forth). The document events get called on application startup, but your extension may not get started up
until well after the application starts and will never receive the initial document event calls. It may be necessary
to call the same code that you call from the document events from inside your extension Startup method as well,
before continuing.

If your extension implements IExtensionConfig, do not assume in your IExtensionConfig code that the extension
is fully initialized; the extension startup may not have been called yet. For example, if your JIT extension is not
currently started in the ArcGIS application when a user opens the Extensions dialog box, the Startup method for
your extension will not have been called yet, so in the members of IExtensionConfig, your code cannot rely on
any state you set during the IExtension::Startup method. As a general rule, you may want to avoid creating any
coclasses in the extension class initialization and defer until IExtension::Startup is called.

Command items, toolbars or menus, or in fact any other classes that need to find their extension should be
careful when calling IApplication::FindExtensionbyCLSID. For example, a command should find its extension in
the ICommand::OnCreate method, instead of in its class initialization code. This is because the first time
FindExtensionByCLSID is called, the extension will be created and IExtension::Startup will be called, which a JIT
extension wants to avoid until necessary. Avoid using IAppliciation::FindExtensionByName, as this may not work
for JIT extensions until the extension is created.

If your command delivers commands, make sure that you do not do any initialization beyond bitmap, caption,
name, and message within the command constructor. You should use the ICommand::OnCreate method to do all
other necessary initialization at the later stage.

Developing Custom Extensions


With a custom extension, you have full control over what happens when your extension is turned on or off. However, it
is a good idea to follow the same general procedures as the existing ArcGIS extensions. The following notes explain
how the ArcGIS extensions work when they are turned on or off in the Extensions dialog box.
When a user checks one of the ArcGIS extensions in the Extensions dialog box, the following things occur:

The checked state of the extension is saved to the user settings in the registry. This is done by the application
it is not the responsibility of the extension.

The extension requests a license from the license manager.

If a license is available, the tools are enabled on the toolbar delivered by the extension.

If a license is not available, the tools are disabled on the toolbar delivered by the extension. Also, text stating
that the license is unavailable is displayed to the right of the extension name in the Extensions dialog box.
(Again, this is done by the applicationit is not the responsibility of the extension).

When a user unchecks one of the ArcGIS extensions in the Extensions dialog box, the following things occur:

The extension verifies that it is not being used within that application.

If the extension is being used, the extension does not allow itself to be unchecked and a warning message is
given.

If the extension is not being used within the application, the uncheck completes successfully and the remaining
steps below occur.

The unchecked state of the extension is saved in the user settings in the registry. (Again, this is done by the
applicationit is not the responsibility of the extension).

If the toolbar for the extension is active, the appropriate tools, commands, and so on are disabled.

The extension lets the license manager know it is no longer using the extension license within the application,
and the license manager releases the license for that application.

The IExtensionConfig interface is independent of ESRI's licensing implementation, so as a developer you can
incorporate a custom licensing solution of your choice. Alternatively, if your extension does not work with a license

98

manager, you may not have to worry about requesting and releasing a license. You can implement IExtensionConfig to
enable and disable the tools on your extension's toolbar accordingly.
Application startup sequence
When working with extensions and document events, it is important to have an understanding of the application
startup sequence. The basic startup sequence is:
1.

User starts the application.

2.

Application object created.

3.

Document object created.

4.

Extensions are loaded.

5.

If a document file is specified on the command line, or if the application is started by double-clicking a document
file, then that document is loaded. If not, a new document is created. If the user then chooses to open an
existing document, that document is loaded.

The order of extension loading cannot be controlled. The extensions are loaded in CLSID order using the appropriate
component category. In certain circumstances, you may want to share data between extensions. In such
circumstances, the data should not be associated with one extension, but instead with another helper class. Each
extension can then check to see if the helper object has been created, and if not, the extension can create it. Once the
helper object is created by the first initialized extension, the other extensions can access the data it contains. Any
document-specific code should not be placed in the extension-loading stagethe extensions are loaded before any
document is opened.
Note that for JIT extensions, this sequence is not applicableread the information above for information on JIT
extension startup.
See Also Extending the Framework, Commands and Tools Example.

DDE Command Handler Example


DDE handler example

Design Coclass DDEHandler implements IDDECommandHandler.


License ArcView or above.
Libraries Carto, Framework, ArcMapUI.
Languages Visual Basic, Visual C++
Categories ESRI Mx DDE Command Handlers
Interfaces IDDECommandHandler
How to use
1.

Register the DDEHandlerVB.dll and run the DDEHandlerVB.reg registry script to register the class to the
required component category.

2.

Open ArcMap and add a feature layer.

3.

Select a few features in the layer.

4.

Run the DDEVBClient.exe application and click the Send DDE Request button.
ArcMap will now pop up a message box that informs you of the number of selected features in the map.

Why create a DDE command handler?


Within ArcMap there are numerous situations where you might want to receive communications from another nonArcGIS application. Users wanting to communicate with the ArcView 3.x architecture in particular may have this
requirement.
One method of communicating between an ArcGIS application and a second application is to make use of the AppROT
coclass. AppROT can be used to access a list of all the currently running ESRI COM applications, including ArcMap,
ArcCatalog, and ArcScene. A reference to AppROT can be embedded in the second application, allowing the second
application to programmatically access the ESRI application.
The use of AppROT can be problematic, however, as AppROT is running out of process. It is inappropriate to share
information about process-specific items, for example, window handles (generally known in code as hWnds). There
may also be threading issues if attempting to hold a reference to and program against the same object from two
different processes.

99

In cases where the use of AppROT is not feasible or appropriate, a useful alternative is a DDE command handler.

What is DDE?
Dynamic Data Exchange (DDE) is a relatively simplistic method of interapplication communication. DDE relies on a
standard underlying windows protocol. Using this DDE protocol, it is possible to send messages and values between
applications.
DDE is a protocol that can be used to communicate between applications. ArcView 3.x users in
particular may be familiar with DDE.
Applications can use the DDE protocol for one-time data transfers, or for continuous exchanges in which applications
send updates to one another when new data becomes available.
Typically, information travels from the DDE source to a destination. Some applications allow data to travel from the
destination back to the source; however, ArcMap does not support the return of data.
DDE in ArcMap
ArcMap has an existing DDE command handler, GNetCommandHandler, which is used to intercept incoming DDE
messages whenever a Geography Network file is opened from, for example, Windows Explorer.
The process of opening the file will start ArcMap, if it is not already running, then send a DDE string message to
ArcMap, specifying the name of the file to be opened.
A custom DDE command handler can be used to execute commands in ArcMap in response to a
request from another application.
You can also create your own DDE command handler, which can operate in parallel with existing handlers.
The DDE command handler mechanism
The mechanism for using DDE to communicate with ArcMap is twofold:

ArcMap is a 'DDE server'it receives incoming requests and responds if appropriate.

3rd Party Application is a 'DDE client'it sends requests to a DDE server.


ArcMap can act as a DDE server, processing incoming DDE requests.

A DDE conversation may proceed as follows.


1.

DDE client starts a conversation with ArcMap by specifying an Application name (for example, ArcMap.exe) and a
conversation type. For more information on conversation types, see 'Setting up a DDE conversation' later in this
section.

2.

Client sends a string message to ArcMap.

3.

ArcMap receives the string message, then looks in the ESRI MX DDE Command Handlers component category to
identify the registered DDE command handlers.

4.

An instance of the first registered handler is instantiated by ArcMap, and the handler's CanExecute method is
called, passing in the incoming string message.

5.

The handler parses the string to check if it is able to use the information it contains. If the string can be used by
the handler, it will return true as the result of the CanExecute function.

6.

Upon receiving a true value from a handler's CanExecute method, ArcMap will call Execute for that handler,
again passing in the same message string.

7.

Steps 4 and 5 are repeated until a handler has returned a value of true from CanExecute.
After calling Execute once, in response to a successful CanExecute, ArcMap will not instantiate any further DDE
command handler coclasses.
Only one DDE command handler can execute each message from the client.

100

Creating a DDE command handler

A basic implementation of a DDE command handler coclass should be straightforwardit only needs to implement the
IDDECommandHandler interface and be registered to the ESRI MX DDE Command Handlers component category.
Complexity may be added by the requests your handler can parse and the functions it can perform upon request.
Implementing IDDECommandHandler
IDDECommandHandler is straightforward to implement. The CanExecute and Execute methods will be called by
ArcMap when incoming DDE messages are received. Each method will be passed the string parameter Command,
which originates from the DDE client.
As implied by the methods on this interface, only the Execute DDE message is supported by ArcMapthe Poke, Request,
and Send DDE messages are not supported.
In this example, the incoming Command string is divided into two parts: an identifier and an action. The identifier and
action are separated by a colon (:). The identifier substring determines which DDE command handler coclass the
incoming Command string is intended to be used by. The action substring determines what action should be taken by
the handler object upon receipt of the Command message.
A single message string identifies both a specific DDE command handler and the action the handler
should execute. This message is passed to the CanExecute and Execute methods of
IDDECommandHandler.
This example uses a member variable, m_strSep, to store the separator string.
[Visual Basic 6]

Private Const c_sSep As String = ":"


The CanExecute procedure first parses the Command string to retrieve the identifier substring, which is the part before
the separator colon. If the identifier received matches the expected identifier for this class (in this case
ARCMAPDEMO), then the CanExecute method returns true.
[Visual Basic 6]

Private Function IDDECommandHandler_CanExecute(ByVal Command As String) _


As Boolean
IDDECommandHandler_CanExecute = False
Command = StrConv(Command, vbUnicode)
Command = TrimNulls(Command)
Command = Trim(Command)
Command = UCase(Command)
Dim strKey As String
strKey = "ARCMAPDEMO"
Dim iPos As Long
iPos = InStr(Command, c_sSep)
If iPos > 0 Then
If Left(Command, iPos - 1) = strKey Then
IDDECommandHandler_CanExecute = True
End If
End If
End Function
The example demonstrates a simple DDE command handler. The handler parses a DDE message and
alters the current selection in ArcMap based on the information contained in the message.
Note that the function also trims any white space at the beginning or end of the string. This may help ensure that your
string functions operate as expected; for example, if a user enters a DDE command string in a TextBox, it is common
for an accidental keystroke to add unexpected extra white space to the beginning or end of the string, which may not
be noticed by the user.
You should take care to ensure that this string is as unique as possible, to ensure a DDE message intended for your
handler is not accidentally intercepted by another DDE handler. Also, this will ensure your DDE handler will not
incorrectly attempt to execute DDE messages intended for other handlers. You might want to use a combination of
your company name and an ID number or name, the DDE handler coclass name, or even its GUID.
Ensure your DDE message is not intercepted by another DDE handler class and that your DDE
handler does not intercept messages intended for another handler.

101

Once the CanExecute method has returned true, then the Execute method will automatically be called by ArcMap,
passing in the Command string.
The Execute method is where the real work is done. This time the action is parsed (the second part of the string, after
the colon), which determines what action is to be carried out.
In the following example code, you can see that the action performed is either counting or clearing the selected
features in the focus map.
[Visual Basic 6]

Private Sub IDDECommandHandler_Execute(ByVal Command As String)


...
Dim iPos As Long
iPos = InStr(Command, m_strSep)
Dim strAction As String
strAction = Mid(Command, iPos + 1)
Dim pApp As esriFramework.IApplication
Set pApp = esriFramework.New AppRef
Dim pMXD As esriArcMapUI.IMxDocument
Set pMXD = pApp.Document
Dim pAV As esriCarto.IActiveView
Select Case strAction
Case "COUNTSELECTION"
Dim iCnt As Long
iCnt = pMXD.FocusMap.SelectionCount
MsgBox Selected features in focus map: " & iCnt
Case "CLEARSELECTION"
pMXD.FocusMap.ClearSelection
Set pAV = pMXD.FocusMap
pAV.Refresh
MsgBox "Selection Cleared"
Case Else
MsgBox "Unrecognized DDE Command"
End Select
End Sub
If the action string is not recognized, then a warning is given.
Note that the code here assumes that the DDEHandler is running inside the ArcMap process and uses the AppRef
object. If you adapt this example, and there is a chance that your class may be used outside ArcMap, using AppRef
may cause errors. You may wish to refer to Chapter 2, 'Developing Objects', for information on a technique to avoid
the instantiation of AppRef outside of ArcGIS applications.
See Chapter 2, 'Developing Objects', for more information about using the AppRef object.
String length
It is possible that long DDE message strings may be truncatedyou may want to include a string terminator character
to check that your message has not been truncated.
Check your DDE message to be sure that it is not too long.
Dealing with Unicode and ANSI strings
Depending on the type of DDE client, the Command string may be either a Unicode or ANSI string. For example,
Windows API calls may return either ANSI or Unicode strings. ArcView 3.x has the ability to handle both ANSI and
Unicode strings, but a DDE message sent from Avenue will be an ANSI string.
If you are creating the command handler in VB, you must be aware that VB natively deals with Unicode strings.
Therefore, any incoming ANSI strings must be converted to Unicode before any parsing of the string takes place.
The type of string received by your handler class may differ depending on the type of client that
creates the message string. You must take care to ensure the received string can be read correctly.
You can convert a string to Unicode in VB by using VB's StrConv function as shown.
[Visual Basic 6]

Command = StrConv(Command, vbUnicode)


Once this Unicode conversion has taken place, the command string may have null characters at the end, which may
cause VB string functions to operate incorrectly. Therefore, you need to remove these null characters from the string
before using it.

102

The following function can be used in VB to trim the extra characters from the end so that subsequent string
comparisons will work correctly.
[Visual Basic 6]

Public Function TrimNulls(inputStr As String) As String


Dim i As Long
i = InStr(inputStr, vbNullChar)
If i > 0 Then
TrimNulls = Left$(inputStr, i - 1)
Else
TrimNulls = inputStr
End If
End Function
Embedded null characters can have unexpected effects on string function operation. They may occur at the end of a
string as shown in the example code, but can also occur mid-string in some cases. For further information on
embedded null characters, see 'Smart Types' in ATL Internals.

Setting up a DDE conversation


Finally, when you have your ArcMap application running, you can call your DDE handler from a DDE client, passing an
appropriate command string to your DDE command handler.
The client should use the following DDE settings for a conversation with ArcMap:

Application: ArcMap
Topic: System
Link Mode: Manual

Conversation types
There are various types of DDE conversation possible, but ArcMap only supports the Manual type. In addition,
remember that ArcMap only supports DDE Executethis will send the specified command string to the server
application.
ArcMap supports a Manual DDE conversation only.
The accompanying VB project, DDEVBClient, demonstrates a simple use of the handler. It uses a TextBox control to
define the Command string and a CommandButton to initiate the DDE conversation with ArcMap via the TextBox
control.
[Visual Basic 6]

Text1.LinkTopic = "ArcMap|System"
Text1.LinkMode = vbLinkManual
Dim strDDE As String
strDDE = Text1.Text
Text1.LinkExecute strDDE

103

104

Chapter4: Creating Cartography


TOC Views
Creating Custom TOC Views
Introduction to how TOC views work and how to design a custom TOC view.
CatalogView Example
An example of a custom TOC view, which displays the Catalog in a table of contents tab.
Creating different kinds of TOC views
Advice on creating other kinds of custom TOC views.
Elements
Creating Custom Elements
Introduction to the Elements object model and creating custom elements.
InfoText Element Example
An example of a custom element, which adds text automatically to a view.
Creating different kinds of custom Elements
Advice on creating different types of custom elements and implementing other element interfaces.
Map Grids
About Map Grids
Introduction to the Grids object model and creating custom map grids.
Clippable Index Grid Example
An example of a custom index map grid, which can be clipped to a shape.
Layers
Creating Custom Layers
Introduction to the Carto Layer object model and creating custom layers.
Simple Point Layer Example
An example of a custom layer, which displays a data format unsupported by ArcMap.

Creating Cartography
TOC Views
Creating Custom TOC Views
Introduction to how TOC views work and how to design a custom TOC view.
CatalogView Example
An example of a custom TOC view, which displays the Catalog in a table of contents tab.
Creating different kinds of TOC views
Advice on creating other kinds of custom TOC views.
Elements
Creating Custom Elements
Introduction to the Elements object model and creating custom elements.
InfoText Element Example
An example of a custom element, which adds text automatically to a view.
Creating different kinds of custom Elements
Advice on creating different types of custom elements and implementing other element interfaces.
Map Grids
About Map Grids
Introduction to the Grids object model and creating custom map grids.
Clippable Index Grid Example
An example of a custom index map grid, which can be clipped to a shape.
Layers
Creating Custom Layers
Introduction to the Carto Layer object model and creating custom layers.
Simple Point Layer Example
An example of a custom layer, which displays a data format unsupported by ArcMap.

105

Creating Custom TOC Views


Table of contents views in ArcMap
The table of contents (TOC) window in ArcMap provides users with a graphical interface, allowing them to interact with
the layers in a map. The TOC appears adjacent to the main map window by default and contains a number of different
views, which are viewed one at a time by selecting the tabs at the bottom of the TOC.

Display view is used to order and display the properties of layers.


Source view is used to view the data source for layers and tables.
Selection view is used to display and interact with the selected features of the map.

ArcMap has three different table of contents views.


How views are loaded by ArcMap
Using the ArcMap default settings, the TOC window is always displayed when you open ArcMap. The views shown in
this TOC window will depend on whether a new document is created or an existing document is opened.
By default, the Display, Source and Selection table of contents views are visible.
TOC views in a new document
If you start a new ArcMap session without loading an existing map document, the application reads from the
Normal.mxt template, which will load the default views. All views can also be toggled on or off if necessary.
TOC views in an existing document
If you start ArcMap and load an existing map document or template, the default settings stored in the Normal.mxt file
are overridden, and all of the views present when the document was saved will be shown. Therefore, if three views
were open (for example, Display, Source, and Selection) when the document was last saved, all three views will be
visible.
A document saves which views are currently visible.
The TOC in ArcMap is displayed by the TOCDockableWindow coclass. Each TOC view
is not responsible for adding a tab window to the TOCDockableWindow; rather, it
must create the client window that will be displayed inside the tab in the TOC
window.

106

Setting the view properties

The basic properties of each view are controlled by the TOCPropertyPage, which you can open by clicking the Tools
menu, then clicking Options. This page provides users with the functionality to change the visible TOC views as well as
the font, patch style, and patch sizes used to display the items in the TOC.

The TOCPropertyPage displays the TOC options and allows you to specify whether or not each view is
visible.

Designing a custom TOC view


A custom TOC view is an application-level customization which affects many parts of the application framework. The
customization should not change the default behavior of the ArcGIS applications in general.
A custom TOC view is an application-level customization. See Chapter 3 for more information on
general application customization rules.
Application state
The status of the application can change during the application, for example, in response to user events such as
opening and loading new documents. Users may also change from the map view to the page layout view.
Your TOC view should, therefore, be robust enough to handle both map and page layout view, and handle the opening
of existing documents and the creation of new documents. It may need to react to certain events to handle these
situationsyou may need to implement appropriate event interfaces to react to such changes.
A custom TOC view should handle both data view and page layout view. It should also handle the
creation and opening of documents.
Efficient loading
Every time ArcMap starts up, an instance of each different TOC view currently registered to ESRI Contents Views is
automatically loaded and available for the duration of the application's execution. Ensure that your TOC view does not
have code, which takes a particularly long time to execute at this point; test your component and consider caching or
other techniques if required.
As a TOC view is around for the lifetime of the ArcMap application, ensure the application's
performance is not degraded by the TOC view.
Generic design
The existing Display and Source TOC views contain functionality that is generic in natureeach will work for any type
of dataset that can be loaded into the document. Taking the Display view as an example, this view can render all the
layer properties, such as the name, visibility, and symbology, regardless of the type of layer that was added to the
map, including any special renderer symbols that have been applied. Furthermore, the views work the same whether
ArcMap is currently displaying the data or page layout view.
The Selection view is slightly less generic in nature. It can display any selectable map layer and will safely handle and
ignore any nonselectable layers.
To avoid any adverse behavior in the application, you should model your view after these generic designsyou must
support or safely ignore any layer that can be loaded into ArcMap or opened from within a document. If your TOC
iterates the layers in a map, never assume a layer's type.

107

A TOC view should be able to handle any kind of data that can be loaded into a map.
See Also CatalogView Example, Creating different kinds of TOC views, and Creating Cartography.

TOC Catalog View Example

Description This project provides a custom contents view for ArcMap, displaying a GxTreeView; datasets from this
view can be dragged and dropped from the TOC onto the map.
Design CatalogView class implements IContentsView and contains an instance of GxTreeView. A helper class,
GxApplication, implements IGxApplication.
License ArcView or above.
Libraries ArcMapUI, Catalog, CatalogUI, Geometry, and System.
Languages Visual Basic, Visual C++; discussion follows the VB implementation.
Categories ESRI Contents Views.
Interfaces IContentsView, IGxApplication.
How to use
1.

If using VB, register TocVB.dll and double-click the TocVB.reg file to register the TOC view class to the
required component category.
If using VC++, first register the TocVB.dll. Then open and build the project TocVC.dsp to register the DLL
and also to register the TOC view class to the required component category.

2.

Open ArcMap.

3.

You should see a tab named Catalog in the TOC. Click this tab.
You can now browse to a dataset using the TOC, choose a dataset, and drag-and-drop it onto the map to
add the data as a new layer.

The case for a custom TOC view


The standard ArcGIS configuration offers two different ways to browse datasets on disk and add them to ArcMap.

You can open ArcCatalog, which allows you to browse data in a tree view. You can then drag-and-drop the selected
datasets into ArcMap.
Alternatively, you can open the standard GxDialog in ArcMap by clicking the Add Data button. You can browse data

108

sets in the dialog box one folder at a time and select datasets to add to the map.
However, many users may want to use the convenient browsing of the ArcCatalog tree view but may not find it
convenient to open an entirely separate application, for example, if layers are continually being added and removed
from the map.
Such a customization is clearly application-level, as users always require access to this functionality. Implementing the
solution as a TOC would ensure that screen `real estate' is conserved; a new dockable or overview window would
require extra screen space, but the TOC window is always available. The customization would be applicable to any data
source, as it is independent of the layers already in a map. Last, the solution would ideally be available from both map
and page layout views. Therefore, it seems that a custom TOC view may be an appropriate solution for the
requirements.
Creating a table of contents view to add data to the map ensures the functionality is always present
while preserving screen real estate.

Creating a tree view


The requirements for this example state that you must provide browse and drag-and-drop access to datasets, similar
to that shown in the tree view in ArcCatalog.

By reviewing the ArcCatalog object model and the online reference, you can see that the GxTreeView coclass provides
ArcCatalog with its browsable tree view of data. You will make use of this class to create your custom TOC view. Since
the objects in the tree view and the map both support drag-and-drop functionality, it will also be possible to drag data
directly from the tree view into ArcMap.
For the GxTreeView to function, the Activate method must be called and references to valid GxApplication and
GxCatalog objects must be passed to it. When the GxTreeView is activated inside ArcCatalog (via the Activate
method), this connects the tree view to its parent application. However, you do not have an instance of ArcCatalog
available, and to create one would defeat the purpose of the customization.
The GxTreeView coclass provides the tree view of data used in ArcCatalog.
The GxApplication coclass

To successfully call the IGxView::Activate method on the GxTreeView, you will create a helper class called
GxApplication, that implements IGxApplication and contains an instance of a GxCatalog.
Full details of the implementation of the GxApplication helper class can be found in the accompanying source code as
its implementation is not directly relevant to the creation of a TOC view. The source code shows how to implement the
minimum functionality to allow the GxApplication class to function correctly.
You will create a helper class, GxApplication, to allow the GxTreeView to be activated.
Creating a subtype of TOCView

Looking at the ArcMap object model, you can see that the existing TOC viewsTOCCatalogView, TOCDisplayView, and
TOCSelectionVieware all subtypes of the TOCView abstract class.
The primary interface implemented by all TOCView classes is IContentsViewthis interface provides the main TOC view
functionality. You can also see that a TOCView does not need to be clonable or persistable.
The existing TOC coclasses also sink the event interfaces IComPropertySheetEvents, IActiveViewEvents and
IDocumentEvents.
The TOCView abstract class is the basis for all table of contents views.
Creating the CatalogView

109

To achieve the requirements described, you will create a class called CatalogView and implement the IContentsView
interface. You will register this class to the ESRI Contents Views component category, which will allow the system to
create a TOC tab and embed your TOC view onto it at runtime.
As the CatalogView does not need to respond to changes in the active view, in relation to document events or in
response to property page changes, you will not implement any of these interfaces. However, the section
'Implementing Different Kinds of TOC Views' later in this chapter gives advice on how you might implement these
interfaces, if you adapt this example to create different custom TOC view implementations.
The CatalogView class will create an instance of a GxTreeView. To correctly Activate this GxTreeView instance, you will
also create a class called GxApplication to emulate an instance of ArcCatalog, which implements the IGxApplication
interface (see earlier section 'The GxApplication Coclass').
You will create a CatalogView class, which will display a GxTreeView in the ArcMap table of contents.
Setting up the view
The majority of the work required in a TOC view can be done in the class initialization code.
1.

Declare member variables to hold IGxApplication, IGxCatalog, and IGxView references.


[Visual Basic 6]

Private m_pGxApp As IGxApplication


Private m_pGxCatalog As IGxCatalog
Private m_pGxView As IGxView
2.

When the CatalogView class is initialized, create an instance of the custom GxApplication class and store
references to this object and its Catalog and TreeView properties.
[Visual Basic 6]

Private Sub Class_Initialize()


Set m_pGxApp = New TOCView.GxApplication
Set m_pGxCatalog = m_pGxApp.Catalog
Set m_pGxView = m_pGxApp.TreeView
3.

Activate the tree view ready for use by calling the IGxView::Activate method on the m_pGxView member
variableyou will need to use the reference to the custom GxApplication object in order to activate the tree
view.
[Visual Basic 6]

m_pGxView.Activate m_pGxApp, m_pGxCatalog


4.

Store the window handle in another member variableit will be used later by IContentsView::hWnd.
[Visual Basic 6]

m_lHWnd = m_pGxView.hwnd
5.

In the class termination function, release the object references.


[Visual Basic 6]

Private Sub Class_Terminate()


Set m_pGxCatalog = Nothing
Set m_pGxView = Nothing
Set m_pGxApp = Nothing
End Sub
Implementing IContentsView
For a TOC view the only interface that must be implemented is IContentsView. This interface has all the controlling
members that allow the TOC view window to be activated and deactivated by the system.
IContentsView is the only mandatory interface for a TOC view class.
Activation and deactivation of the window
Only one TOC view may be active in ArcMap at any given time. When the system or a user activates a view, the
IContentsView::Activate method of the appropriate view is called. A reference to the current document (Document) is
passed in to the method along with the handle of the parent window (parentHWnd).
Declare the Windows API function ShowWindow and the constants for showing and hiding windows using this function.
[Visual Basic 6]

Private Const SW_HIDE = 0


Private Const SW_SHOW = 9
Private Declare Function ShowWindow Lib "user32" (ByVal hwnd As Long, _
ByVal nCmdShow As Long) As Long
Place the GxTreeView onto the screen by calling the Windows API function ShowWindowpass in the window handle you
stored in the class initialization code.
[Visual Basic 6]

110

Private Sub IContentsView_Activate(ByVal parentHWND As esriSystem.OLE_HANDLE, ByVal Document As


esriArcMapUI.IMxDocument)
ShowWindow m_lhwnd, SW_SHOW
End Sub
When a custom TOC view is activated, it should display its window. You can use Windows API calls to
show and hide the GxTreeView.
ShowWindow can be used to change the state of any window, for example, to minimize, maximize, hide, or show a
windowwhere you use it to show the GxTreeView window on the screen.
IContentsView::Deactivate is called by the system on the currently active view when a user or code selects a different
TOC view.
[Visual Basic 6]

Private Sub IContentsView_Deactivate()


ShowWindow m_lHWnd, SW_HIDE
End Sub
If you stored a reference to the current MxDocument by using the reference passed to Activate or by sinking events
interfaces, you should also release the reference at this point.
When a TOCView is called to deactivate itself, you should hide the GxTreeView window. You should
also release any references to any items in the document or map.
The handle of a TOC view window is returned to ArcMap via the IContentsView::hWnd property. In this case, you
should pass the handle of the GxTreeView using the member variable that you set in the Activate method.
[Visual Basic 6]

Private Property Get IContentsView_hWnd() As esriSystem.OLE_HANDLE


IContentsView_hWnd = m_lHWnd
End Property
Other members of IContentsView
Some of the members of IContentsView were designed to be used particularly by the Display and Source view, as they
apply particularly to the behavior and functionality provided by these views. However, you should add code to the
implementation of at least the Name, SelectedItem, and Visible properties, which are discussed below.
Although it is not essential for every custom TOC view to fully implement each member of
IContentsView, you should implement at least the Name, SelectedItem, and Visible properties.
The Name property must return the text that identifies your TOC view tab in the TOCDockableWindowit is a good idea
to keep this short for display purposes.
[Visual Basic 6]

Private Property Get IContentsView_Name() As String


IContentsView_Name = "Catalog"
End Property

The Name property is used in the TOCDockableWindow as the tab caption.


The Visible property relates to the settings in the TOC tab of the Options dialog box. This dialog box displays all the
currently registered TOC views. Users can select and deselect each TOC view to determine if the view is displayed in
the TOC. This setting is stored at the document level. By default the CatalogView is visible, but you can allow users to
change this by storing a boolean value.
[Visual Basic 6]

Private m_bIsVisible As Boolean


...
Private Property Let IContentsView_Visible(ByVal bValue As Boolean)
m_bIsVisible = bValue
End Property
The Visible property allows users to turn a TOC view on and off.
The SelectedItem property is designed to link TOC views with other calling code. The TOCDisplayView, for example,
may return either a reference to a map layer, legend item, or map frame depending on which item is selected.
From SelectedItem, return a reference to the SelectedObject of the contained GxApplication.
You will implement IContentsView::SelectedItem to allow other code to access the currently selected item in the
CatalogView. The SelectedItem property passes a Variant back to the calling object; therefore, any type of object can
safely be returned; in this case, SelectedItem returns an IGxObject reference.
[Visual Basic 6]

Private Property Get IContentsView_SelectedItem() As Variant


If (m_pGxApp Is Nothing) Then Exit Property

111

If (Not m_pGxApp.SelectedObject Is Nothing) Then


Set IContentsView_SelectedItem = m_pGxApp.SelectedObject
Else
Set IContentsView_SelectedItem = Nothing
End If
End Property
The GxTreeView only allows a single object to be selected at any one time; therefore, you can only return a single
item from this property.
The Refresh method is called after a TOC view is activated. It is also called by the system at certain other timesfor
example, when layers are added to the map. The TOC view should update its contents at this point. In CatalogView,
simply forward the refresh call to the contained GxApplicationthis will ensure that any changes in the file system (for
example, data which has been created, deleted, or moved) are reflected in the view.
[Visual Basic 6]

Private Sub IContentsView_Refresh(ByVal Item As Variant)


If (Not m_pGxApp Is Nothing) Then m_pGxApp.Refresh ("")
End Sub

Plugging CatalogView into ArcMap


Once the component is compiled, you need to register the CatalogView to the ESRI Contents View component
category. See Chapter 2, 'Developing Objects', for more information on how you can register to component categories.

When ArcMap starts it creates a list of all the TOC views in this category. It then creates a new display tab in the TOC
window for each view.
For each view, if IContentsView::Visible returns True, then that view will automatically be made visible in the TOC.
After the TOC view window is created, ArcMap will use the hWnd property to embed your client window into the TOC
tab view.
After registering your TOC view, you should find your TOC looks something like that shown here.
Go to example code
See Also Creating different kinds of TOC views, Creating Custom TOC Views, and Creating Cartography.

Creating different kinds of TOC views


In the previous topic you saw how to implement the CatalogView, a basic TOC view with straightforward functionality.
You may want to add further functionality to this example, or you may want to implement an entirely different type of
TOC view, using this as a starting point. The following sections discuss other issues of TOC view implementation not
used in this example.
Selecting multiple items
Other members of IContentsView, such as ContextItem, AddToSelectedItems, and RemoveFromSelectedItems, can be
implemented to manage a collection of multiple selected items.
In the CatalogView example, only a single selected item is allowed, and therefore, the class does not maintain a set of
selected items. This is because the GxTreeView itself only allows the selection of a single GxObject at one time.
However, the example could be adapted to allow a user to select more than one item at once by using the approach
discussed below, which bases a TOC view on a standard tree view control. The GxObjectArray coclass is suitable for
creating and managing an enumeration of GxObjects that may be selectedreturn a reference to the enumerator from
IContentsView::ContextItem.
If a user is able to select multiple items in your TOC view, you should fully implement the
IContentsView members ContextItem, AddToSelectedItems, and RemoveFromSelectedItems.

112

Refreshing a TOC view


A client may call Refresh to force a TOC view to update itself at any point, for example, after data has been added to
the map or after the initial activation of the view.
Refresh may be called by a client to indicate that a TOC view should update its contents.
To implement the Refresh method, first check the value of the incoming Variant parameter. If only one item needs to
be acted on to perform the refresh, the calling function will pass in this one item. Your TOC should interrogate this
item and make the appropriate changes to the view.
For example, the TOCDisplayView receives a map layer during the Refresh method when a layer is added to the map.
This allows the TOCDisplayView to update the contents of the Legend by adding a LegendItem for that layer.
You may want to set the value of ProcessEvents to True while your TOC view is dealing with a call to Refresh to
prevent other code from executing (see below).
Synchronizing the view with changes in ArcMap
The CatalogView does not need to respond to changes in ArcMap, as the display of the tree depends on the data
available and not on the contents of the map or document.
However, if you decide it is appropriate for your TOC view to respond to changes in ArcMap by sinking event
interfaces, you will need to correctly use the IContentsView::ProcessEvents property, described below.
To respond correctly to changes in ArcMap, you should use the ProcessEvents property of
IContentsView together with event interfaces.
Generally, a TOC view should always check its ProcessEvents value before beginning potentially time-consuming
processing.
Each member of a sinked event interface should check the value last passed to ProcessEvents to
determine whether or not to perform any actions in the view.
The client (ArcMap) uses ProcessEvents on the currently active TOC view to suspend the actions in the TOCView.
Later, the TOC view can again synchronize the state of an object in the TOC view with the state of that object
somewhere else in the application.
For example, a TOC class may sink the IActiveViewEvents interface to update itself when a user adds or removes map
layers. The Map will inform the TOC view it needs to be updated, allowing the TOC view to synchronize itself with the
appropriate objects. ArcMap sets the ProcessEvents property of the active TOC view to False before displaying the Add
Data dialog box to suspend changes while the dialog box is displayed. After adding the data to the map and
completing the redraw, ArcMap will then set ProcessEvents back to True, indicating to the TOC view that it can now
process information from events.

When to use IDocumentEvents and IActiveViewEvents


The existing TOC views respond to changes in ArcMap by sinking the IDocumentEvents interface. Changes within the
map document are responded to by sinking the IActiveViewEvents interface. Although this example did not require this
functionality, you could implement these interfaces if required.
If you store a reference to the current document (passed in to IContentsView::Activate), you should a minimum
implement IDocumentEvents, as you will need to keep this reference up-to-date if the current document is changed.
Sinking events interfaces may help your TOC view synchronize with the changes in ArcMap.
You should sink IDocumentEvents if you store a reference to the current document in order to keep
this reference up-to-date.
When to use IComPropertySheetEvents
The existing TOC views also sink the IComPropertySheetEvents interface. The OnApply member of this interface is
called when changes in its associated property sheet have been applied by the user.
If your custom TOC view implementation includes a property sheet, you may want to sink this interface also.
Sink IComPropertySheetEvents if you provide a property sheet for your TOC view.
Using an alternative tree view
Throughout this book, it is most common to provide user interface components by creating a modeless form that
contains various user controls. The handle of the form or of an individual control can then be returned as the client
window.

113

This approach can typically be seen in many example implementations of IToolControl, where the handle of a control
or form is returned via the IToolControl::hWnd property. Any window handle can be embedded in the view itself,
although most often the handle returned belongs to a single ActiveX control or to a form or picture box that acts as a
container for multiple controls.
This example, however, uses a somewhat different approach. The GxTreeView coclass is used to provide the client
window, although it is not an ActiveX control. The GxTreeView class has been used here for convenience. Its use does
impose certain limitationsthe behavior of the view is fixed, only one item can be selected, a GxApplication helper
class must be created, and the GxTreeView must be re-created each time the TOC is selected.
The CatalogView example differs from many examples in this book, as it provides a visual
component without using a form, dialog box, or control.
You can adapt the example to display a form or control if required.

As an alternative solution, you can use an ActiveX control, for example, the standard tree view control, as the window
of the CatalogView.
Add a form to your project, and place an ActiveX control on the form. Return the handle of this control from the
IContentsView::HWnd property.
This approach offers much greater flexibility and control over the viewyou can control the exact appearance and
behavior of the tree view. You can display or exclude anything you want, allowing you to create a user-customizable
view of the data.
However, you should consider the additional coding that would be required to implement the view from scratchyou
would need to traverse the GxCatalog and add the appropriate GxObjects to the tree.
Each custom TOC view will require different components, depending on the functionality required and the information
or items that need to be displayed. For example, you may decide to write a custom TOC view that can display a
calculation of the area of selected features after a selection is performedyou could use a rich text box to display this
information and return its window handle as the hWnd property.
See Also CatalogView Example, Creating Custom TOC Views, and Creating Cartography.

Creating Custom Elements


Creating a subtype of Element
You can see from the ArcMap object model diagram that the existing element coclasses are all subtypes of the Element
abstract class.
Any custom element, therefore, should implement a minimum of IElement, IElementProperties, IBoundsProperties,
and ITransform2D. IElementProperties2 may also be implemented for completeness, although this is not essential for
an element to function.
In addition, elements should always implement IClone and either IPersist and IPersistStream or IPersistVariant,
depending on your development environment. You may also want to implement IPropertySupport, as this will increase
compatibility with existing graphics tools; however, it is not mandatory and cannot be implemented in VB.
Elements are clonable and persistable. They are stored in the document.
FrameElement or GraphicElement
In the object model diagram, elements are split further, with coclasses inheriting either from the GraphicElement or
FrameElement abstract classes.
Your next design decision should be whether your custom element is a FrameElement or GraphicElement.

114

A FrameElement is an element that implements IFrameElement and forms a border around other elements or objects.
Many FrameElements, such as MapFrame and TableFrame, can only appear on a page layout.
A GraphicElement draws simple graphic shapes, pictures, or text, for example, the MarkerElement, LineElement, and
TextElement. The IGraphicElement interface adds the ability for an element to appear in either page layout or data
view.
Some elements, such as GroupElement and BMPPictureElement, implement both IFrameElement and IGraphicElement.
They can appear both as simple graphics and can also draw with a surrounding frame and can be placed in either a
page layout or data view.
ArcGIS uses different kinds of elements.
Some elements can only appear in page layout view. GraphicElements can be added to a map and
will account for changes in the map's coordinate system. FrameElements have a surrounding
neatline.
General design issues for a custom element
Below is a brief review of some design decisions you might need to make when creating a custom element.

Does the element need to appear in the data view? If so, create a subtype of GraphicElement. Does the element
need a neatline to surround it? If so, create a subtype of FrameElement.

Do you need access to the current map for the element to draw or behave correctly? If so, you should consider
using VC++ and create a subtype of MapSurround instead.

If you decide your graphic element needs access to the current document (like this example), make sure your
element can degrade its behavior safely if instantiated in a process outside ArcMap, for example, the MapControl
or PageLayoutControl.

When deciding which existing element interfaces to implement, in addition to the functionality you want to add,
consider which existing property pages will apply to your element (see the sections on element property pages
later in this section).

See Also InfoText Element Example, Creating other types of custom elements, and Creating Cartography.

115

Info Text Element Example


Object Model Diagram

Description The project provides a graphic element, which adds text automatically to a page layout or map. The text
can report the current user, computer name, map document path, author of the document, and list of templates. The
property pages allow the user to select what text is required and to change the appearance of the text.
Design InfoTextElement is a subtype of the Element abstract class, with accompanying property page coclass
InfoTextPropertyPage. A command is also included (NewInfoTextCommand) to add the element to the active view
License ArcView or above.
Libraries ArcMapUI, Carto, Display, DisplayUI, Framework, Geometry, System, and SystemUI.
Languages Visual Basic.
Categories ESRI Element Property Pages, ESRI Mx Commands
Interfaces IElement, IElementProperties, IBoundsProperties, IGraphicElement, ITextElement, IClone, IPersistVariant,
and ITransform2D.
How to use
1.

If using VB, register InfoTextElementVB.dll and double-click the InfoTextElementVB.reg file to register to
component categories.

2.

If using VC++, open and build the project InfoTextElementVC.dsp to register the DLL and register to
component categories.

3.

Open ArcMap.

4.

Open the Customize dialog box, click the Commands tab, click 'Extending ArcObjects' in the left-hand list,
and drag the NewInfoText tool onto a toolbar. Close the Customize dialog box.

5.

Click the New InfoText tool and click-and-drag a rectangle on the map.
This will create a new InfoTextElement on the map. Right-click the element to see the property pagetry
changing the settings to change the information displayed or the font used.

The case for a custom Graphic Element


A typical map consists of many different elements. As well as the geographical features of a map, many additional
elements help the map to communicate its purpose. North arrows, legends, scalebars, and titles are all common
elements of a well-annotated map.
In addition to these, you may want to add other items to help explain a map's purpose and contentexplanatory text,
diagrams, flow charts, arrows, and so forth.

116

In many applications such as Microsoft Word and Excel, it


is also possible to add header and footer information to a
view or page, which allows you to easily add information
such as the location of a document on disk, the current
user and computer, and so on, which can be especially
helpful when printing out maps in large organizations.
In ArcGIS, you can add a graphic element containing text
to your map or page layout to list such information.
However, a standard text element simply draws a static
string of text. If the document is moved or saved to a
different location, the text stored in the element will not
reflect these changes. If another user opens the
document, they will need to update the information.
If the document is opened on a different machine, these
changes will also need to be made to the text.
You could create a command or macro that updates this
information. However, you would either need to ensure
the macro was run as appropriate by customizing the
normal template or ensure the update function was run
when opening the document.
Alternatively, you could create a custom graphic element,
which automatically adds the required text and keeps the
information up-to-date.

Creating the Info Text Element

To solve the requirements of this example, you will create a subtype of GraphicElement, called InfoTextElement. This
class adds a piece of text to a map or page layout, reporting the current user, computer name, document path,
document author, and the templates used. You will provide the ability to switch off each piece of information
independently.
You will implement IElement, IElementProperties, IBoundsProperties, and ITransform2D, as well as the standard
interfaces for cloning and persistence. For maximum flexibility, the element you will create should be able to appear in
either page layout or data view and will, therefore, create a class that implements IGraphicElement. As you will be
drawing text, a separate frame is not requiredTextSymbols have their own backgrounds. Therefore, you will not
implement IFrameElement.
The InfoTextElement will add information automatically to a map.
Although the element will display text, you will not implement ITextElement the Text property page (displayed for
classes that implement ITextElement) should not apply to the InfoTextElement, as users should not be able to change
the actual text of the element themselves. However, like the existing TextElement, you will sink the ITransformEvents
events interface. This will allow you to provide correct scaling behavior of your element when the view scale changes;
see the `Implementing ITransformEvents' section for more details.
To add the custom functionality and to allow the element to be identified programmatically, you will also create and
implement a custom interface called IInfoElement.
To allow users to add an InfoTextElement to a dataframe, you will create an ArcMap Command. To allow users to
change the properties of an InfoTextElement in the UI, you will also create a property page for your element.
Now you will look in more detail at each interface and see how to implement the important members of the
InfoTextElement coclass.
The example project also includes a property page for the element and a custom tool to allow users
to create new InfoTextElements in ArcMap.
Creating and Implementing IInfoElement
Your InfoTextElement needs to be able to calculate the required information automatically. You must also provide a
way for users to specify which bits of information should be included in the displayed text and to change the
TextSymbol used to draw the text.
To achieve these goals, create an interface called IInfoElement. Add five read-write boolean properties to the
interface, called ShowUser, ShowComputer, and so on. Add another read-write property to allow clients access to the
Symbol and a read-only property to allow quick access to the current Text for convenience.
The custom IInfoElement interface will allow clients to specify which information is displayed by the
element. It also allows clients to identify instances of InfoTextElement.

117

Now implement IInfoElement on the InfoTextElement 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 ShowAuthor property
below.
[Visual Basic 6]

Private Property Let IInfoElement_ShowAuthor(ByVal RHS As Boolean)


m_bShowInfo(3) = RHS
End Property
The value of the Show properties and the Symbol property are initialized in the class initialization code and later will be
set by the property page you will create in the 'Plugging your custom element into ArcMap' section later in this topic.

Calculating the text values


Next, you will calculate the automatic text of your custom elementfor efficiency, the values will be calculated as little
as possible.

The Windows username will not change in an InfoTextElement object's lifetime. Therefore, the username is
retrieved in the class initialization code using the GetUserName Windows API call.

A computer's name could possibly change, if the user changes the name of the computer while using ArcGIS;
however, this is unlikely. Therefore, the computer name is only retrieved in the class initialization code using the
GetComputerName Windows API call.
The GetUserName and GetComputerName Windows API call can be used to find the current
Windows user name and computer name.

The current path and name of a document will change if a user saves to a different name or locationthis value
is, therefore, updated in the persistence Save method by checking the IApplication::Templates property of the
current application.

A base template cannot be added once a document is created; therefore, the templates are only checked in the
class initialization code (again using IApplication::Templates).

IDocumentInfo::Author may change at any point. There is no way to identify when a user has made a change to
the document's properties, so Author is repeatedly checked in the GetAutoText function.
The document path, author, and list of templates are found through the running application,
which relies on the element being inside an ArcMap process.

Next, you will create the GetAutoText function, which will return the automatic text of your custom element, based on
the values of the IInfoElement properties. For example, if the ShowUser property is true, the first piece of text to
appear will be the username of the current user, which is cached at initialization.
[Visual Basic 6]

Private Function GetAutoText() As String


Dim sTemp As String
If m_bShowInfo(0) Then
sTemp = "User: " & m_sUser
GetAutoText = GetAutoText & vbNewLine & sTemp
End If
...
To complete the path, templates and author information, your element requires access to the currently running
Application object. Add a member variable to the element class and set it in the class initialization code, after which
the document path, templates, and author can be determined.
[Visual Basic 6]

Private m_pApplication As esriFramework.IApplication


If an InfoTextElement is instantiated outside ArcMap (for example, in a map control), there will be no running
Application. See the `Coding Interface Members' section in Chapter 2, `Developing Objects', for more information
about the technique used to identify the running process and obtain an Application reference safely.
AppRef is used to get a reference to the current document. As elements may be instantiated outside
the ArcMap process, all the element code needs to account for this without causing errors.
To see how the rest of the information is calculated, see the GetUserString, GetComputerString, GetDocPath,
GetTemplates, and GetAutoText functions in the accompanying example code.
Determining the available information options
Last, add a read-only property internal to the project called EnableAppOptions. You will use this later from the
property page you will create.
[Visual Basic 6]

Friend Property Get EnableAppOptions() As Boolean


EnableAppOptions = Not (m_pApplication Is Nothing)
End Property

118

Implementing IElement
IElement provides clients with access to the shape of an element. It also provides functions for drawing and
performing hit tests on the element.
IElement provides properties and methods based on the shape of an element.
To begin, implement the Geometry property to simply store a reference to a clone of the geometry passed inwhen a
user interacts with an element (for example, by moving it around in a view), the system will set the element's
Geometry property with the new shape.
Check that the geometry type is appropriate for the elementa Point is sufficient to locate an InfoTextElement, as the
height and width of the element will be determined by the font.
[Visual Basic 6]

Private Property Let IElement_Geometry(ByVal pGeometry As esriGeometry.IGeometry)


If TypeOf pGeometry Is esriGeometryType.esriGeometryPoint Then
Set m_pGeometry = CloneMe(pGeometry)
End If
End Property
User interaction with a SelectionTracker

Next, create a selection tracker object. This will be used by ArcMap to allow users to interact with your element in the
ActiveView. The element will always be rectangular, unless it is rotated; therefore, you will use a PolygonTracker
instead of an EnvelopeTracker, as an Envelope is not rotatable.
Add a member variable to hold a selection tracker object to your class. Then initialize the tracker in your class
initialization code.
[Visual Basic 6]

Private m_pSelectionTracker As esriDisplay.ISelectionTracker


...
Private Sub Class_Initialize()
Set m_pSelectionTracker = New PolygonTracker
m_pSelectionTracker.Locked = False
m_pSelectionTracker.ShowHandles = False
...
You will return this tracker object from the SelectionTracker property.
[Visual Basic 6]

Private Property Get IElement_SelectionTracker() As esriDisplay.ISelectionTracker


Set IElement_SelectionTracker = m_pSelectionTracker
End Property
Whenever a change is made to the size, shape, or location of an element, this change must be reflected in its selection
tracker. To do this, create a routine called RefreshTracker. First, update the Display property of the SelectionTracker
from the cached Display m_pDisplay (see the Activate method for information on when this variable is set), as it may
have changed since the last time the tracker was refreshed. There will be no cached Display until the initial call to
Activate, so check this member before using it. Then use the QueryOutline method of IElement to calculate the new
shape of the tracker. You will implement QueryOutline later.
[Visual Basic 6]

Private Sub RefreshTracker()


If m_pCachedDisplay Is Nothing Then Exit Sub
Set m_pSelectionTracker.Display = m_pCachedDisplay
Dim pOutline As esriGeometry.IGeometry
Set pOutline = New esriGeometry.Polygon
IElement_QueryOutline m_pCachedDisplay, pOutline
m_pSelectionTracker.Geometry = pOutline
End Sub
To reflect changes in the element's shape, RefreshTracker needs to be called when the Geometry property is set and
also in the Activate method. Later, you will also use RefreshTracker in the members of ITransform2D.
The Geometry held by a SelectionTracker is set by value. Therefore, each time the shape or location

119

of an element changes, the tracker's Geometry must be updated.


You, therefore, need to update the geometry of the tracker when the element's Geometry or Symbol
changes. You will also need to update the tracker from other interface members you will implement
later.

Element activation and deactivation


When a user activates a new view, elements are informed of the change by the Activate and Deactivate methods. For
example, when you switch from data view to page layout view, Deactivate will be called on all elements in the data
view, and Activate will be called on all elements in the page layout view; this process happens in reverse when the
views are switched back. Activation and deactivation also occur at other points, for example, when you activate a
different dataframe. These methods give elements the opportunity to allocate or deallocate resources, connect to other
objects, or cache display settings.
When a user selects a new view, Deactivate is called on the elements in the previous view, then
Activate is called on the elements in the selected view. At this point, elements can allocate or
deallocate resources.
In Activate, the main action you should take is to store a reference to the currently activated screen
display which is passed in. You will need to use this later in other members of IElement and other
interfaces.
For the InfoTextElement, the Activate method just needs to cache the passed-in Display, then update the selection
tracker with this new reference.
[Visual Basic 6]

Private Sub IElement_Activate(ByVal Display As esriDisplay.IDisplay)


Set m_pCachedDisplay = Display
RefreshTracker
End Sub
In Deactivate you can release the reference to the cached Display.
[Visual Basic 6]

Private Sub IElement_Deactivate()


Set m_pCachedDisplay = Nothing
End Sub
After the view in which an element resides is activated, the element will be drawn as the view is refreshed. Draw is
straightforward to completesimply draw the text to the display.
[Visual Basic 6]

Private Sub IElement_Draw(ByVal Display As esriDisplay.IDisplay, _


ByVal TrackCancel As esriSystem.ITrackCancel)
Display.SetSymbol Nothing
Display.SetSymbol m_pTextSym
Display.DrawText m_pPointGeometry, m_sAutoText
End Sub
The Display passed to Draw will be a reference to the same object that was passed to Activateunless the view is
currently being exported or printed. Therefore, always use the passed-in reference to perform the Draw.
You will also need to use an IDisplay reference to complete the Draw, QueryOutline, and
QueryBounds members. These members receive an IDisplay reference directly, which should be used
in preference to the cached reference, as the element may be requested to draw to a printer or
output file, instead of the currently active view.

Boundaries and outline of an Element


In the QueryOutline client-side storage property, you need to populate a Polygon with the shape of the element,
accounting for its current Text, Symbol and Geometry. First, clear any existing shape from the Outline parameter,
update the element text, and set a reference to the current DisplayTransformation.
[Visual Basic 6]

Outline.SetEmpty
m_pTextSym.Text = GetAutoText
Dim pTransform As esriDisplay.IDisplayTransformation
Set pTransform = Display.DisplayTransformation
If you are working in VB, take particular care with your object references when coding the client-side
storage members QueryOutline and QueryBoundary.
To return the outline of the element, you will need to QI for ISymbol on the TextSymbol. Use the QueryBoundary
method and the Geometry of the element to calculate the outline of the element.
[Visual Basic 6]

Dim pSym As esriDisplay.ISymbol


Set pSym = m_pTextSym

120

pSym.QueryBoundary frmResource.hDC, pTransform, m_pGeometry, Outline


Use ISymbol::QueryBoundary method to calculate the outline of an element.
You can make use of the QueryOutline method when you complete the QueryBounds methodcall QueryOutline, and
populate the Bounds parameter by using the IGeometry::QueryEnvelope method.
[Visual Basic 6]

Private Sub IElement_QueryBounds(ByVal Display As esriDisplay.IDisplay, _


ByVal Bounds As esriGeometry.IEnvelope)
Dim pOutline As esriGeometry.IGeometry
Set pOutline = New esriGeometry.Polygon
IElement_QueryOutline m_pCachedDisplay, pOutline
pOutline.QueryEnvelope Bounds
End Sub
It is worth noting that in many cases, QueryBounds is used by clients as an alternative to QueryOutline. For many
elements, QueryBounds gives a rougher approximation of the shape of an element and is correspondingly more
efficient to call. If a custom element has a QueryOutline method, which may be time-consuming (especially if called
frequently in a loop), consider creating a more efficient QueryBounds method if an approximation can be easily
calculated.
If possible, consider coding QueryBounds as a faster, rougher approximation of the same of an
element than QueryOutline. You may find it useful to use Windows API calls to quickly approximate
the extent of a piece of text.
You can also make use of the QueryOutline method when coding the HitTest method. First, create a Point object from
the x and y coordinates passed in. Then retrieve the outline of the element, and use the IRelationalOperator's Disjoint
method to find out if the Point lies inside the outline.
[Visual Basic 6]

Private Function IElement_HitTest(ByVal X As Double, ByVal Y As Double, _


ByVal Tolerance As Double) As Boolean
Dim pPt As esriGeometry.IPoint
Set pPt = New esriGeometry.Point
pPt.PutCoords X, Y
Dim pOutline As esriGeometry.IRelationalOperator
Set pOutline = New esriGeometry.Polygon
IElement_QueryOutline m_pCachedDisplay, pOutline
IElement_HitTest = Not pOutline.Disjoint(pPt)
End Function
ArcMap determines if a user is trying to select a particular element by calling the HitText method to
see if the mouse coordinates lie inside the outline of the element.
The Locked property was designed for use with graphic elements that are stored in a read-only geodatabase, and
therefore, the InfoTextElement ignores the value passed to Locked.
Annotation elements (elements that implement IAnnotationElement) can implement the Locked property by retrieving
the Feature associated with the annotation element, and checking the associated Workspaceif the workspace is
currently being edited, then Locked should return false, and the Geometry of the element should be updatable.
Implementing IGraphicElement
Implementing IGraphicElement allows an element to appear in a dataframe, as it can account correctly for the
coordinate system of the dataframe. Features store their Geometry and SpatialReference independently and are
reprojected on-the-fly when drawn to a dataframe that has a different SpatialReference; however, elements are
assumed to be in the same SpatialReference as the view in which they are displayed.
IGraphicElement allows an element to be projected to any coordinate system. It provides correct
behavior for an element in the data view.
When the SpatialReference property is set, project the Geometry to the new SpatialReference; there is no need to
check if the two SpatialReference values are equal, as Project will perform this check internally. Note that the
SpatialReference property of an Element is held separately to the SpatialReference of the Element's Geometry, in the
member variable m_pNativeSpatialRef.
[Visual Basic 6]

Private m_pNativeSpatialRef As esriGeometry.ISpatialReference


...
Private Property Set IGraphicElement_SpatialReference(ByVal _
SpatialReference As esriGeometry.ISpatialReference)
' pSpatialReference may

be null

Set m_pNativeSpatialRef = SpatialReference


UpdateElementSpatialReference

121

End Property
An element's Geometry should always have a SpatialReference the same as the current
DisplayTransformation.
Now add the UpdateElementSpatialRef routine to perform the projection.
[Visual Basic 6]

Private Sub UpdateElementSpatialReference()


If Not m_pNativeSpatialRef Is Nothing Then
If Not m_pPointGeometry Is Nothing Then
If m_pPointGeometry.SpatialReference Is Nothing Then
Set m_pPointGeometry.SpatialReference = _
m_pCachedDisplay.DisplayTransformation.SpatialReference
End If
m_pPointGeometry.Project m_pNativeSpatialRef
RefreshTracker
End If
End If
End Sub
The SpatialReference of an element may not be set on the first call to this propertyin this case, you can use the
SpatialReference of the cached Display as the initial native spatial reference of the Element.
The SpatialReference property of an element may not always be set before it is usedyour code
needs to account for this.
Implementing IElementProperties
All elements, frames, and graphics implement the IElementProperties interface; it provides functionality generally used
by developers to identify elements and store custom properties on the element.
IElementProperties mainly provides ways for a programmer to add different types of information to
an element. However, the AutoTransform property is used by the ITransform2D interface.
For the Name, Type and CustomProperty members, you should store and return data as required. For the Name
property, simply allow a user to store a string. To be consistent with existing elements, this property is null by default.
From the Type property return a string indicating the class of elementby default return "InfoTextElement".
CustomProperty should hold an empty variant by default.
AutoTransform indicates which aspects of the element should be affected by using the ITransform2D interface. If
AutoTransform is False, a transformation should only affect the Geometry of an element; if True, an element should
also transform its Symbol or other properties as appropriate. For the AutoTransform property, simply return or store a
boolean valueyou will use it later when implementing ITransform2D.
[Visual Basic 6]

Private Property Let IElementProperties_AutoTransform(ByVal AutoTransform _


As Boolean)
m_bAutoTrans = AutoTransform
End Property
Implementing IElementProperties2
IElementProperties2 should always be implemented on a custom Elementnewer commands and tools may use this
interface. This interface duplicates all the members of IElementProperties and adds two new ones. Return True from
CanRotate, because text can be rotated to any angle, by setting the ITextSymbol::Angle property.
IElementProperties2 replicates the members of IElementProperties, and adds properties to determine
how an element's Symbol is treated.
For the ReferenceScale property, return or store a double value indicating the reference scalethis value will be
accounted for by the DisplayTransformation.
[Visual Basic 6]

Private m_dRefScale As Double


Implementing IBoundsProperties
IBoundsProperties is used to determine how an element can be scaled. Return True from the read-only FixedSize
property indicates that the InfoTextElement is an element whose size is determined not by its Geometry, but by its
Symbol.
[Visual Basic 6]

Private Property Get IBoundsProperties_FixedSize() As Boolean


IBoundsProperties_FixedSize = True
End Property
You should also return True from the FixedAspectRatio property (and ignore any attempts to set the property),
because if an element has a fixed size, its aspect ratio must also be fixed.

122

If FixedSize returns False, the Fixed Aspect Ratio check box on the Size and Position property page will be enabled; if
FixedAspectRatio is True, the check box will be checked. The property page will calculate size and position changes
based on these settings.
The Fixed Aspect Ratio check box on the Size and Position property page uses the IBoundsProperties
interface to determine its availability and value.
Implementing ITransform2D
The Size and Position property page uses an element's ITransform2D interface to change an element; ITransform2D is
also used in the element's context menu by the Nudge, Rotate and Flip, Align, and Distribute context-menu
commands.

The ITransform2D interface allows an element to be moved, rotated, and scaled. The Size and
Position property page uses the ITransform2D::Transform method to change height, width, and
origin of an element. The other ITransform2D members are used by other ArcMap commands and
tools.
For the Move and MoveVector methods you can simply forward the call to the ITransform2D interface of the element's
Geometry and refresh the tracker after the transformation.
[Visual Basic 6]

Private Sub ITransform2D_Move(ByVal dx As Double, ByVal dy As Double)


Dim pTransform2D As esriGeometry.ITransform2D
Set pTransform2D = m_pPointGeometry
pTransform2D.Move dx, dy
RefreshTracker
End Sub
As the InfoTextElement has a Point Geometry, rotation of the Element will not affect the orientation of the text itself;
therefore, you should also check the value of the AutoTransform property you stored when implementing
IElementProperties.
[Visual Basic 6]

Private Sub ITransform2D_Rotate(ByVal Origin As esriGeometry.IPoint, _


ByVal RotationAngle As Double)
Dim pTransform2D As esriGeometry.ITransform2D
Set pTransform2D = m_pPointGeometry
pTransform2D.Rotate Origin, RotationAngle
If m_bAutoTrans Then
m_pTextSym.Angle = NewRotateAngle(RotationAngle)
End If
RefreshTracker
End Sub

If AutoTransform is True then the Symbol should also be rotatedset the Angle of the TextSymbol by adding the new
rotation value to the existing Angle.

123

[Visual Basic 6]

Private Function NewRotateAngle(ByVal dAngle As Double) As Double


NewRotateAngle = m_pTextSym.Angle + RAD2DEG(dAngle)
End Function
Private Function RAD2DEG(ByVal Radians As Double) As Double
RAD2DEG = Radians * (180# / PI)
End Function
ITransform2D methods should check the value of the AutoTranform propertyif True, the element's
Symbol needs to be transformed as well as the element's Geometry.
You can complete the Scale and Transform methods in a similar manner by first transforming the Geometry and
accounting for the AutoTransform value. Note that the Scale method cannot scale both height and width, as the
InfoTextElement has a fixed aspect ratio.
[Visual Basic 6]

Private Sub ITransform2D_Scale(ByVal Origin As esriGeometry.IPoint, _


ByVal sx As Double, ByVal sy As Double)
Dim pTransform2D As esriGeometry.ITransform2D
Set pTransform2D = m_pPointGeometry
With pTransform2D
.Scale Origin, sx, sy
End With
If m_bAutoTrans Then
If sy <> 1 Then
m_pTextSym.Size = m_pTextSym.Size * sy
ElseIf sx <> 1 Then
m_pTextSym.Size = m_pTextSym.Size * sx
End If
End If
RefreshTracker
End Sub
The Transform method needs to account for translation, scaling, and rotation.
Implementing ITransformEvents

The majority of elements determine not only their location but their size and shape by their Geometry. Text-based
elements are differentthe location is determined by the Geometry, but the shape and size are determined by the
current TextSymbol. This results in unexpected behavior for the SelectionTracker of a text-based element when the
map scale is changed, as the SelectionTracker after the scale change will have a Geometry that is incorrect for the new
map scale.
To correct this behavior, you can process the BoundsUpdated event of the current DisplayTransformation.
By sinking the outbound ITransformEvents interface of the DisplayTransformation, you can update
your element to reflect changes such as the dataframe being rotated. You can also use
ITransformEvents to update the tracker geometry correctly when the map scale changes.
Add a member variable to store the default outbound interface of DisplayTransformation, ITransformEvents.
[Visual Basic 6]

Private WithEvents m_pDisplayTrans As DisplayTransformation


Now hook up this variable to the DisplayTransformation in Activate.
[Visual Basic 6]

Private Sub IElement_Activate(ByVal Display As esriDisplay.IDisplay)


Set m_pCachedDisplay = Display
Set m_pDisplayTrans = Display.DisplayTransformation

124

RefreshTracker
End Sub
Now refresh the tracker in the BoundsUpdate event.
[Visual Basic 6]

Private Sub m_pDisplayTrans_BoundsUpdated(ByVal sender As _


esriDisplay.IDisplayTransformation)
RefreshTracker
End Sub
Implementing IClone, IPersistStream, and IPersistVariant
Cloning and persistence functionality are essential for any element. Your InfoTextElement should, therefore,
implement IClone. If you are working in VC++, you should also implement IPersist and IPersistStream; if working in
VB, implement IPersistVariant.
Elements must be clonable and persistable. See Chapter 2, 'Developing Objects', for general
information on coding cloning and persistence methods.
In the Save persistence method, don't forget to update the document path, as saving the document may change this
value. There is no need to persist the references to the current Application, SelectionTracker, or cached Display, as
these will be set when the Element is re-created.
[Visual Basic 6]

Private Sub IPersistVariant_Save(ByVal Stream As esriSystem.IVariantStream)


m_sDocPath = GetDocPathString
m_pTextSym.Text = GetAutoText
Stream.Write m_lCurrVers
Stream.Write m_pPointGeometry
Stream.Write m_sElementName
Stream.Write m_sElementType
Stream.Write m_dRefScale
Stream.Write m_pNativeSpatialRef
Stream.Write m_pTextSym
Stream.Write m_bShowInfo(0)
...
End Sub
You may also want to persist the IElementProperties CustomProperty method, in which case you would need to check
if the set variant contains a persistable data type.

Plugging your custom element into ArcMap


Your custom element class is now ready to be used programmatically. However, to improve the usability of the
element, there are two more issues you should consider. Using the ArcMap user interface, users should be able to
create your element, add it to a document, and edit the properties of the element.
Creating a new InfoTextElement in ArcMap
If you are working in data view, you can add standard graphic elements to a document by selecting the appropriate
shape from the Drawing Tools button on the Drawing toolbar, then tracking the element's shape onto the view as
required. Alternatively, to add text or callouts, use the Text Tools button.

Elements are created in ArcMap by using either the Drawing Tools or Text Tools tool on the Drawing
toolbar.
If you are working in layout view, you can again use the drawing toolbar to add graphic elements, or use the Insert
menu to add other types of elements such as a Neatline (FrameElement).
These commands and tools are hardcoded to create each type of graphic or frame elementthere is no component
category that contains elements. You must, therefore, create a new command or tool to add a custom element to the
ActiveView.
As the InfoTextElement is a graphic element, you will create a new tool that allows users to click on the ActiveView at
the point they want to place an InfoTextElement. This behavior is similar to that used by the New Text tool.

125

Creating the NewInfoTextTool

Add a new class to your project called NewInfoTextTool and implement the ICommand and ITool interfaces in that
class. In the ICommand::OnCreate method, store a reference to the Application.
[Visual Basic 6]

Private m_pApp As esriFramework.IApplication


...
Private Sub ICommand_OnCreate(ByVal Hook As Object)
Set m_pApp = Hook
End Sub
You should perform the majority of the work for this tool in the ITool::OnMouseDown method. If the left button has
been clicked, create a Point in Map units. Set the Point's SpatialReference property to that of the Map.
[Visual Basic 6]

If Button = 1 Then
Dim pPoint As esriGeometry.IPoint, pMxApp As esriArcMapUI.IMxApplication
Set pMxApp = m_pApp
Set pPoint = pMxApp.Display.DisplayTransformation.ToMapPoint(X, Y)
Dim pMxDoc As esriArcMapUI.IMxDocument, pMap As esriCarto.IMap
Set pMxDoc = m_pApp.Document
Set pMap = pMxDoc.ActiveView.FocusMap
Set pPoint.SpatialReference = pMap.SpatialReference
Create a new InfoTextElement in the tool's OnMouseDown method. This can be used as the
Geometry of a new InfoTextElement.
Next, create a new InfoTextElement, set its Geometry to the Point you just created, and QI for the
IDocumentDefaultSymbols interface of the current MxDocument to set the IInfoElement::Symbol property.
[Visual Basic 6]

Dim pElement As esriCarto.IElement, pInfoEl As IInfoElement


Set pElement = New GraphicElementVB.InfoTextElement
pElement.Geometry = pPoint
Set pInfoEl = pElement
Dim pDefaultSymbols As esriArcMapUI.IDocumentDefaultSymbols
Set pDefaultSymbols = pMxDoc
Set pInfoEl.Symbol = pDefaultSymbols.TextSymbol
Default symbols for any new graphic element should generally be taken from the
IDocumentDefaultSymbols interface.
Add the InfoTextElement to the GraphicsContainer of the ActiveView, select the new element, and use a PartialRefresh
to redraw that area of the view.
[Visual Basic 6]

Dim pGCont As esriCarto.IGraphicsContainer


Dim pGContSelect As esriCarto.IGraphicsContainerSelect
Set pGCont = pMxDoc.ActiveView.GraphicsContainer
pGCont.AddElement pElement, 0
Set pGContSelect = pGCont
pGContSelect.UnselectAllElements
pGContSelect.SelectElement pElement
pMxDoc.ActiveView.PartialRefresh esriViewGraphics, pElement, Nothing
Last, to be consistent with other tools that create new elements, set the CurrentTool in ArcMap to be the Select
Elements tool.
[Visual Basic 6]

Dim pItem As esriFramework.ICommandItem, u As New esriSystem.UID


u = "{C22579D1-BC17-11D0-8667-0000F8751720}"
Set pItem = m_pApp.Document.CommandBars.Find(u)
Set m_pApp.CurrentTool = pItem
Provide standard implementations of all other members of ICommand and ITool. Register the command to the ESRI

126

Mx Commands component category.


Using the NewInfoTextTool
In ArcMap, add the command to a toolbar by opening the Customize dialog box and dragging the command onto any
toolbar. Select the NewInfoTextTool, then click a location on a view to add a new InfoTextElement at that location.

Right-click the element and choose Properties to view the element properties dialog box.

Creating a property page for the InfoTextElement


To edit the properties of any graphic element in ArcMap, click the pointer tool and right-click the graphic element in
either data or layout view. Alternatively, select a number of graphic elements before right-clicking to see the
properties common to all the selected elements.
Existing element property pages
The Properties dialog box will check for property pages in up to three component categories. Pages registered to ESRI
Element Property Pages are always checked. If the element or elements being edited implement IMapFrame, the
dialog box will also check pages registered to ESRI Map Property Pages; if the element or elements implement
IFrameElement, the dialog box will also check pages registered to ESRI Frame Element Property Pages. The dialog box
will return, containing all the pages for which Applies returns True.
By implementing IElement, the Size And Position property page will apply to your elementthis page will assume that
any element it receives also supports ITransform2D.
All elements will have the Size and Position property pagethis relies on the IElement and
ITransform2D interfaces.
As the user may have selected a number of elements, all element property pages should be able to cope when passed
an IEnumElement reference instead of an IElement reference. In this case, the properties that have been changed in
the page are applied to all the elements in the enumeration.
Creating the InfoPropertyPage

At this point, your users are only able to alter the properties of the IInfoElement interface programmatically. You will
now create a simple property page to allow users to change which items of text are shown on the element (ShowUser,
and so on) and the Symbol used to draw the text.
Add a class called InfoPropertyPage and a Form called frmInfoTextPropertyPage to your project. Register the class to
the ESRI Element Property Pages component category.

Add check boxes to allow users to set the ShowUser, ShowComputer, ShowDocPath, ShowAuthor, and ShowTemplates
properties individually. Also add a button to change the Symbol, and a text box to display the current font information.
You can use the EnableAppOptions property of the InfoTextElement to selectively disable the Document path, Author
name, and Templates options if the element does not currently reside in the ArcMap application.
The property page form should contain controls allowing users to set all the properties of
IInfoElement.
For full details of the code behind the Form, see the accompanying example code.
Implementing property page interfaces for the InfoPropertyPage
InfoPropertyPage is a standard implementation of a property page. See 'Property Pages' in Chapter 2 for more
information on implementing a property page.
In the Applies method, iterate through the Objects SafeArray parameter and return True if you find an object that
implements IInfoElement.
[Visual Basic 6]

Dim pObj As Variant, i As Long


Objects.Reset
For i = 0 To Objects.Count - 1

127

Set pObj = Objects.Next


If Not pObj Is Nothing Then
If TypeOf pObj Is GraphicElementVB.IInfoElement Then
IComPropertyPage_Applies = True
If the parameter contains an IEnumElement reference, iterate each of the elements in the enumerations, and return
True only if all of the elements in the enumeration implement IInfoElement.
[Visual Basic 6]

ElseIf (TypeOf pObj Is esriCarto.IEnumElement) Then


Dim pElements As esriCarto.IEnumElement, pCurrEl As esriCarto.IElement
Dim bApplies As Boolean
Set pElements = pObj
pElements.Reset
bApplies = True
Do
Set pCurrEl = pElements.Next
If Not (pCurrEl Is Nothing) Then
bApplies = (bApplies And _
(TypeOf pCurrEl Is GraphicElementVB.IInfoElement))
End If
Loop While Not pCurrEl Is Nothing
IComPropertyPage_Applies = bApplies
Exit Function
End If
In the Applies and SetObjects property page methods, an element property page may receive either
a reference to a single element or a reference to an enumeration of elements.
An element property page should only apply (Applies = True) if it can be used to edit all the
elements passed to it.

To manage references to a number of elements and their properties, add a class called InfoElementsCollection to your
project. This will act as a custom collection class; for details of the class, see the code in the accompanying project.
In the property page Form class, declare a member variable m_pElementColl, and provide access to this via a
property.
[Visual Basic 6]

Private m_pElementColl As InfoElementsCollection


...
Public Property Get InfoElements() As InfoElementsCollection
Set InfoElements = m_pElementColl
End Property
Create the InfoElementsCollection class to help the property page manage multiple references to
elements; an element property page may be displayed for more than one element.
In the SetObjects method, create a new InfoElementsCollection.
[Visual Basic 6]

Private Sub IComPropertyPage_SetObjects(ByVal Objects As esriSystem.ISet)


...
Set m_frmPage.InfoElements = New InfoElementsCollection
Again, iterate the Objects to check for objects that implement IElement or IEnumElement. If you receive an
IEnumElements reference, add each element from the enumeration to the ElementCollection of the Form; otherwise,
just add the single IElement to the collection.
[Visual Basic 6]

If (TypeOf pObj Is IInfoElement) Then


m_frmPage.InfoElements.Add pObj, Str(m_frmPage.InfoElements.Count)
ElseIf (TypeOf pObj Is esriCarto.IEnumElement) Then
..
In the Form, apply property changes to each of the members in this collection. If the property page is cancelled, the
changes will be discarded by the property sheet.
SetObjects will also receive references to the Transformation, GraphicsContainer, and PageLayout (if applicable) of the

128

element or elements. Your property page can use these objects, if required, to help edit the properties of an element.
You should add code to your Form to display the properties applicable to all the elements receivedfor example, if two
elements were received, unequal properties can be indicated by graying-out the check box (not disabling it).
[Visual Basic 6]

Dim pInfoEl As GraphicElementVB.IInfoElement, valChecked(5) As Integer


For Each pInfoEl In m_pElementColl
valChecked(0) = valChecked(0) + Int(pInfoEl.ShowUser)
valChecked(1) = valChecked(1) + Int(pInfoEl.ShowComputer)
...
In the code, changes are made directly to the elements. This means that there is no need to work out which controls
have been changed and, hence, which properties need to be updated. Also, the Apply property page method does not
need to do anything.
[Visual Basic 6]

Private Sub SetShowUser(ByVal bShow As Boolean)


Dim pInfoEl As GraphicElementVB.IInfoElement
For Each pInfoEl In m_pElementColl
pInfoEl.ShowUser = bShow
Next pInfoEl
End Sub
Element property pages are not displayed as embedded pages; therefore, there is no need to implement
IComEmbeddedPropertyPage. If you are working in VC++, you can return E_NOTIMPL from CreateCompatibleObject,
although it is good practice to provide a complete implementation.
Compile and register the project again, and you will be able to set the properties of the InfoTextElement using this
new user interface.

Go to example code
See Also Creating other types of custom Element, Creating Custom Elements, and Creating Cartography.

Creating different kinds of custom Element


The InfoTextElement example shows you one way to create a custom element. If you are using this example as a
template to create a different kind of element, you may find it useful to consider the other element interfaces
discussed below.

Creating point, line, and fill graphic elements


1D and 2D elements do not generally need to account for AutoTransformation in their ITransform2D methods as it is
the shape (Geometry) of the element that is transformed here.
Implementing the appropriate symbol interface (IMarkerElement, ILineElement, or IFillShapeElement) will improve
integration with ArcGIS and also automatically display the appropriate Symbol property page to allow users to change
the appearance of the element.
Creating a TextElement
To implement a custom TextElement, you must aggregate the existing TextElement coclass. This requirement restricts
the development environments that you can usesee the discussion of aggregation in the TreeFeature Custom
Feature topic in Chapter 7, Customizing the Geodatabase.
If implementing ITextElement, the Text property should hold the element's text string and set this string into the
element's TextSymbol. In ScaleText, you will need a reference to the view of which the element is a memberyou
could do this by creating a custom tool to create your element that finds the FocusMap and passes it into your element

129

via a custom interface. If ScaleText is True, increase or decrease the size of the element's Font so that the text's size
onscreen remains constant. You also need to account for the reference scale, if one is set.
Creating a FrameElement
If you need to add a neatline around an element, you can implement IFrameElement in either VB or VC++. The Frame
of a frame element generally surrounds another object (IFrameElement::Object) such as a map or legend, which
knows how to draw itself.
FrameElements should also implement IFrameDraw and IFrameProperties. The IFrameDraw interface contains
methods that will be called to draw the parts of the element separately. First, the background
(IFrameElement::DrawBackground) is drawn. Next, if IFrameElement::DraftMode is True, DrawDraftMode will be
calledin this method you should add simple text giving the name of the element; if DraftMode is False,
IElement::Draw will be called, at which point you should draw the contained Object over its background. Last,
DrawForeground will be called at which point you should draw the Frame itself.
By implementing IFrameProperties, the Frame property page will apply to your element. If you create a property page
that can be successfully applied to all frame elements, you should register the page to the ESRI Frame Element
Property Pages component category, instead of ESRI Element Property Pages. For example, existing frame element
property pages provide a user interface for setting the Background and Border of IFrameElement. Otherwise, register
the page to the ESRI Element Property Pages category.
See Also InfoText Element Example, Creating Custom Elements, and Creating Cartography.

About Map Grids


Creating a subtype of MapGrid
By reviewing the ArcMap object model diagram, you can see that all the standard grid coclasses available in
ArcGISCustomOverlayGrid, Graticule, IndexGrid, and MeasuredGridare subtypes of the MapGrid abstract class.

Looking at the MapGrid abstract class, you can see a custom map grid must implement IMapGrid. You also need to
implement IClone and either IPersist and IPersistStream or IPersistVariant, (depending on your development
environment), as grids must be clonable and persistable.
The existing map grids also implement IGraphicsComposite. This interface can be used programmatically to access the
graphic elements that compose the displayed map grid. You will also implement IGraphicsComposite.
The MapGrid abstract class represents a grid of reference points or lines over a Map.
Refer to this abstract class as the starting point for any custom map grid class.

Design issues for a custom grid coclass


Before you actually start coding your custom grid coclass, ask yourself if the functionality you require is similar to that
available in any of the standard ArcGIS grid coclasses.

If you want to use one of the standard grids, but change the way one or two methods or properties work, simply
contain an instance of the required coclass within your custom coclass. Delegate all property and method calls to
the contained object, and adapt as required to change the way the method works. For example, if you require an
IndexGrid that has extra labels, you can contain an instance of IndexGrid in your class and delegate member
calls to this class. On a call to Draw, after calling the contained IndexGrid's Draw, you can add your extra
labelling as required.
You may want to base your custom map grid on an existing map grid class by using
containment.

If you want to add functionality to an existing grid, again, you can use containment to hold an instance of an
existing grid coclass within your custom grid coclass. Add one or more additional interfaces that provide access
to the new functionality. This design is most similar to the approach taken for this example.

If your grid requirements are significantly different from standard grids, you may want to create an entirely new
kind of map grid. In this case you must write most of the behavior yourself but can save time by only
implementing the interfaces required for a basic grid to functionthat is, those listed above for the MapGrid
abstract class.

IIndexGrid and IGraphicsComposite cannot be implemented in VB


IIndexGrid inherits from IMapGrid; therefore, you cannot implement this interface in VB. The VC++ implementation is
described. VB programmers could create a basic map grid by implementing IMapGrid and adding their own custom
implementation of index-type functionality including user interface.
The IIndexGrid and IGraphicsComposite interfaces cannot be implemented in VB.
Map grid factories
Having reviewed the ArcMap object model and the map grid classes, you may have noticed that the existing map grids

130

are all associated with a factory object.


For more information about MapGridFactories, see the section 'Plugging your custom grid into ArcMap'.
See Also Clippable Index Grid Example and Creating Cartography.

131

Clippable Index Grid Example


Object Model Diagram

Example Code Click here


Description The project provides an index grid for a Map that can be clipped to a certain shape. An accompanying
factory coclass allows the map grid to be created by using the standard ArcMap user interface. The property pages
allow the properties of the grid to be set via the ArcMap user interface.
Design ClippableMapGrid is a subtype of the MapGrid abstract class and IndexGrid coclass. ClippableIndexGridFactory
is a subtype of the MapGridFactory abstract class. ClippableGridPage and NewClippableGridPage both implement
standard property page interfaces. A helper coclass, EnumElement, implements IEnumElement.
License required ArcView or above.
Libraries ArcMapUI, Carto, CartoUI, Display, Framework, Geometry, Geodatabase, GeodatabaseUI, System, and
SystemUI.
Languages Visual C++
Categories ESRI Map Grid Factories, ESRI Map Grid Property Pages, and ESRI Map Property Pages
Interfaces IMapGrid, IIndexGrid, IMapGridFactory, and IEnumElement.
How to use
1.

Open the CustomMapGrid.dsp workspace and build the project. This will register the CustomMapGrid.dll
and register coclasses to the required component categories.

2.

Open ArcMap and add a few layers to the map in the default data view.

3.

Zoom the data view to the extent you want to display.

4.

Create a graphic element defining the shape of the grid you require; ensure that graphic element is
selected before continuing.
If you want to define the shape of the grid based on a feature in the map, first use the Select Features tool
to select the feature. Then use the Pointer tool, right-click the graphic, and click Convert Features to
Graphics.

5.

Choose the page layout view, right-click the map frame, and click Properties from the context menu. The
Data Frame Properties dialog box should now be displayed.

6.

Click the New Clippable Index Grid tab and check the Create new clippable index grid check box. Set the
name, columns, rows, and tab style as required.

7.

Click the Use Selected Data Graphic button to set the selected graphic element as the shape of the

132

clippable index grid. Click OK to dismiss the Data Frame Properties dialog box.
You should now be able to see your clippable index grid displayed around your dataframe.
Notes The grid will draw inside the dataframe, instead of around the edge of the dataframe, as is more
usual for other grids. You may want to restrict the extent of the data frame further, once the grid is
displayed. You may also want to remove from the map any features that intersect or fall outside of the
grid by applying a definition query to filter the visible features.
If you used an element for the clip geometry, you may want to return to the data view and delete the
graphic after the grid has been set up. Alternatively, hide the graphic by setting its color to 'No Color'.

The case for a custom map grid


Maps are often presented in a rectangular formatfor example, the pages of a road atlas.
However, geographical featurescities, administrative regions, counties,
countries, and riversare most often irregularly shaped and do not always fit
well into a rectangular frame.
When producing maps in ArcGIS, if the area you are mapping does not conform
to a rectangular frame, you can clip your dataframe to a shape of your choosing,
as shown in the map on the left.
Imagine now that you need to produce an overview map, dividing this map into
sections that indicate the boundaries of a series of more detailed maps, like the
index map that is often given at the start of a road atlas. To satisfy this
requirement, you can create an overview map with an overlaid grid using the
Grids and Graticules wizard in ArcMap.
Generally, in this situation you would apply an index grid (called a reference grid
in the Grids and Graticules wizard) to your map, which is specifically designed for
this requirement. Displayed on a PageLayout, it divides a map frame into a
chosen number of columns and rows, labelling each division along the axes,
allowing each section to be identified clearly.
Your overview map has an irregular shapebut the index grid uses a rectangular
shape. You can see (left) that your overview map will have some empty divisions,
which you do not require and which may be misleading.
Alternatively, you could apply a measured grid or a graticule, dividing the map
into sections based on a chosen map distance or by latitude and longitude. Both
of these grids also use a rectangular grid, and neither is specifically designed for
use as an index map.
By programming with ArcObjects, you have a fourth optionyou could use a
CustomOverlayGrid. This option is not available through the Grids and Graticules
wizard. By using this coclass, you can create a grid based on the line features of
your own data source.
The CustomOverlayGrid, however, labels the grid lines themselves, not the grid
squares created. It is also a somewhat rigid solution because to change the
number of columns or rows in the grid, you are required to edit the line features
on which your grid is based.
As your requirements for an index map grid are not met by the standard map
grids available in ArcGIS, you must create a custom map grid.

Creating the clippable index grid


To solve the requirements of this example, you will create a subtype of IndexGrid,
called ClippableIndexGrid. You will implement IMapGrid and IIndexGrid as well as the
standard interfaces for cloning and persistence. To add the custom functionality, you
will also create and implement a custom interface, IClippableIndexGrid.

As the design is based closely on the standard index grid, you can delegate
many of its members to the members of a contained IndexGrid. You will adapt
the standard functionality of this index grid to create a grid that can follow the
shape of the map data or map framethe most flexible approach being to
allow the grid to be clipped to any chosen shape.
This can be achieved by implementing your own Draw method, instead of
delegating the call to the contained IndexGrid.
To allow users to add a ClippableIndexGrid to a dataframe, you will continue
the example by creating a factory object, which can be used by ArcMap to
create instances of your custom grid. You also need to allow the properties of a
ClippableIndexGrid to be set and edited by a user. Both these issues are dealt

133

with in later sections.


Now that the design of the class is decided, you need to look in more detail at how to implement the important
members of each interface on the ClippableIndexGrid coclass.
Implementing IMapGrid
When your coclass implements IMapGrid, it becomes a map grid and can be treated as such in the ArcGIS system.
As the ClippableIndexGrid class design uses containment, most of the members of IMapGrid can be delegated directly
to the contained IndexGrid coclass.
The members of IMapGrid for which you need to modify behaviorthose which cannot be directly delegated to the
IndexGridare discussed in turn below. For the benefit of those adapting this sample, typical actions that should be
performed in each of the members are also summarized separately in the following table.
IMapGrid members and descriptions
Border

Return or set an IMapGridBorder reference, storing the map grid border.

Draw

You will perform much of the work of IMapGrid in this method. Draw the map grid for a
map frame to the given Display. Draw all the components of the map grid: the grid lines,
ticks, subticks, tick marks, border, and labels.

ExteriorWidth

Return the width (in display units) of the portion of the grid that is outside the frame.

GenerateGraphics

Generate graphic elements corresponding to the grid lines and store them in the specified
graphics container. Your code will be similar to the Draw method, except that instead of
drawing geometries to the display with their respective symbols, the symbols and the
geometries are put into an element and the element is added to a group element.

LabelFormat

Return or set an IGridLabel reference storing the label format for the map grid labels.

LineSymbol

Return or set an ISymbol reference. Use this to draw the grid lines. If this property is null,
you do not need to draw any grid lines.

Name

Return or set a string value indicating the name of the current map grid.

PrepareForOutput

Perform any actions required to prepare the map grid for output to a device. Generally,
you would get the Map associated with the MapFrame parameter. From the Map's
IActiveView interface, you would get the ScreenDisplay; from the ScreenDisplay, get the
DisplayTransformation. Apply the Map's FullExtent as the transformation's Bounds, and the
Map's VisibleExtent as the transformation's VisibleBounds. You would also apply the passed
in PixelBounds as the DisplayTransformation's DeviceFrame.

QueryLabelVisibility

Return values indicating the visibility of the labels along all four sides of the map grid.

QuerySubTickVisibility

Return values indicating the visibility of the subticks along all four sides of the map grid.

QueryTickVisibility

Return values indicating the visibility of the ticks along all four sides of the map grid.

SetDefaults

Reset all the member variables storing properties of the map grid to their default values.

SetLabelVisibility

Set values indicating the visibility of the labels along all four sides of the map grid.

SetSubTickVisibility

Set values indicating the visibility of the subticks along all four sides of the map grid.

SetTickVisibility

Set values indicating the visibility of the ticks along all four sides of the map grid.

SubTickCount

Return or set an integer indicating the number of subticks to draw between the major
ticks.

SubTickLength

Return or set a double indicating the length of the subticks in points.

SubTickLineSymbol

Return or set an ILineSymbol reference storing the LineSymbol used to draw the subtick
lines.

TickLength

Return or set a double indicating the length of the major ticks in points.

TickLineSymbol

Return or set an ILineSymbol reference storing the LineSymbol used to draw the major
ticks.

TickMarkSymbol

Return or set an IMarkerSymbol reference storing the MarkerSymbol used to draw tick
marks at the grid interval intersections. If null, do not draw any tick mark intersections.

Visible

Return or set a Boolean value indicating if the map grid is visible.

The Draw and GenerateGraphics methods


The Draw method is called when a PageLayout containing a clippable index grid is refreshed. In this method, you must
draw all of the elements of the clippable index grid to the specified Display object.
The GenerateGraphics method is called if the user clicks the Convert to Graphics button on the Grids property page of
the Data Frame Properties dialog box. In this method, you need to convert your grid into individual graphic elements.
As the ClippableIndexGrid has a fundamentally different appearance than the standard IndexGrid and does not display
all the items that an IndexGrid would, you must implement these two methods from scratch instead of delegating

134

them.
Much of the internal logic required for these two methods is similar; therefore, you can modularize your code by using
a single internal method to do most of the work for both the Draw and GenerateGraphics methods. In this example,
the internal method DisplayGrid can either draw directly to a Display or add elements to a GroupElement, depending
on the type of parameters it receives.

The creation of the actual appearance of a map grid is done by the Draw and GenerateGraphics
methods.
The Draw method draws the grid to a Display, and the GenerateGraphics method creates a graphic
element for each part of the grid.
The ClippableIndexGrid uses a general function, DisplayGrid, to perform either of these acts.
This design helps you keep all your grid calculation and drawing code in one place, making your code more modular
and easier to update should you need to change how your grid draws.
The following steps describe the main actions of the DisplayGrid function, illustrated by brief extracts of code; the full
code can be found in the accompanying VC++ example project.
For clarity, the code is described as for the Draw method. In the accompanying VC++ project you can see how this
function deals with both drawing to a Display and adding graphic elements to a GroupElement.
The 'clip geometry'
Note that in this section you will use a 'clip geometry'this is the geometry set by the user on which the shape of the
clippable index grid is based. Its value will come from the ClipGeometry property of the IClippableIndexGrid interface,
which you will implement later.
DisplayGrid Part 1preparing the shape of the clipped grid
The first step to displaying a grid is to calculate the shape of the grid and the intervals of the grid lines. To do this, you
will need to transform from Map space to PageLayout space.
1.

Get the properties used for drawing from the contained IndexGrid.

2.

QI the MapFrame for its IElement interface and get its Geometry.

3.

Store the extent of the grid in the variable ipExtent. This extent must be in page units, as it will be used later for
drawing the clipped grid to the PageLayout. The source of this extent depends on whether or not the clip
geometry has been set.
a) If the clip geometry is not specified, the extent is taken from the MapFrame's Geometry from step 2. This
is already in page units.
[VC++]

ipFrameGeometry->get_Envelope(&ipExtent);
b) If a clip geometry is specified, the extent is taken from this Geometrythis geometry is in map units
(ipExtentMap) and must, therefore, be transformed to page units. This transformation has a number of steps
and is explored in depth below.
[VC++]

IEnvelopePtr ipExtentMap;
m_ipClipGeometry->get_Envelope(&ipExtentMap);

First, you will need to get the DisplayTransformation of both the Map and the PageLayout.
[VC++]

IMapPtr ipMap;
pMapFrame->get_Map(&ipMap);
IActiveViewPtr ipMapView(ipMap);
IScreenDisplayPtr ipMapDisplay;
ipMapView->get_ScreenDisplay(&ipMapDisplay);
IDisplayTransformationPtr ipPageTrans, ipMapTrans;
ipMapDisplay->get_DisplayTransformation(&ipMapTrans);
pDisplay->get_DisplayTransformation(&ipPageTrans);

135

m_ipClipGeometry->get_Envelope(&ipExtent);
Next, transform ipExtentMap to page units. The transformation is in two stages, from map to device
units, then from device to page units.

[VC++]

tagRECT pageRect;
ipMapTrans->TransformRect(ipExtent, &pageRect, esriTransformToDevice +
esriTransformPosition);
ipPageTrans->TransformRect(ipExtent, &pageRect, esriTransformToMap +
esriTransformPosition);
You can transform measurements from Map to PageLayout space by accessing the
DisplayTransformation of the MapFrame and Display passed to Draw and
GenerateGraphics.
You can access the appropriate Display object by using the GetScreenDisplay property
of the IActiveView interface of the IGraphicsContainer parameter.
Now that you have the extent Envelopes of the clip geometry in both map units and page units, you can
create an AffineTransformation2D.

[VC++]

IAffineTransformation2DPtr ipAT2D(CLSID_AffineTransformation2D);
ipAT2D->DefineFromEnvelopes(ipExtentMap, ipExtent);
Clone the clip geometry to preserve the shape in map units, then use the ipAT2D transformation you
just created to transform it to page space.

[VC++]

IClonePtr ipClone(m_ipClipGeometry);
IClonePtr ipNew;
ipClone->Clone(&ipNew);
ipClipGeometryPage = ipNew;
ITransform2DPtr ipT2D(ipClipGeometryPage);
ipT2D->Transform(esriTransformForward, (ITransformationPtr)ipAT2D);
The ipClipGeometryPage variable will hold this clip geometry in page units for later use in the
DisplayGrid method.

o
4.

Next, calculate the details of the individual grid cells. From the extent in page units (ipExtent) calculated in step
3, calculate the grid origin and intervalsfor example, the minimum, maximum, and intervals are calculated for
the x-axis below.
[VC++]

double xmin,xmax;
ipExtent->get_XMin(&xmin);
ipExtent->get_XMax(&xmax);
double xOrigin = xmin;
double xInterval = (xmax - xmin) / (double)numColumns;
Once you have calculated the shape and extent of the grid, you can work out the extent of
each grid cell.
5.

Your clipped index grid must only draw the cells of the grid that overlap with the clip geometry.

Create a GeometryBag. You will use this to collect geometries representing the individual cells in the
clipped index grid.

[VC++]

IGeometryCollectionPtr ipGeomCol(CLSID_GeometryBag);
Next, using the clip geometry and the grid cell information calculated in step 4, create a Polygon
representing each cell in the map grid. Use the IRelationalOperator::Disjoint method to figure out which
grid cells have a non-null intersection with the clip geometry.

[VC++]

for (int nRow = 0; nRow < numRows; nRow++) //Iterate Rows of cells
{
...
for (int nCol = 0; nCol < numColumns; ++nCol) //Iterate Columns

136

{
...
VARIANT_BOOL bDisjoint;
ipRel->Disjoint((IGeometryPtr)ipPointCol, &bDisjoint);
if ((bDisjoint == VARIANT_FALSE))
ipGeomCol->AddGeometry((IGeometryPtr)ipPointCol);
Union these cells to get one Polygon, ipClippedCells, which determines the overall shape of the clipped
grid.

[VC++]

IGeometryPtr ipClippedCells(CLSID_Polygon);
ipTopoClippedCells = ipClippedCells;
ipTopoClippedCells->ConstructUnion((IEnumGeometryPtr)ipGeomCol);
ipTopoClippedCells->Simplify();
Remove any inner rings from the Polygon, as they are not relevant to the clippable index grid.

[VC++]

IGeometryCollectionPtr ipRingCol(ipClippedCells);
for (long l = count - 1; l >= 0; --l)
{
ipRingCol->get_Geometry(l, &ipGeom);
ipRing = ipGeom;
ipRing->get_IsExterior(&bExterior);
if (bExterior == VARIANT_FALSE)
{
ipRingCol->RemoveGeometries(l, 1);
}
}
Get the boundary Polyline (ipBoundary) of the Polygon representing the cells in the clipped grid.

[VC++]

IGeometryPtr ipBoundary;
ipTopoClippedCells->get_Boundary(&ipBoundary);
DisplayGrid Part 2drawing the clipped grid
Now you can begin to actually draw the grid. The grid lines, border, and labels are drawn in turn; tick marks are not
drawn as they are not appropriate on an IndexGrid.
Now that you have calculated the shape and size of the grid and its cells, you can begin to display
the grid, starting with the grid lines.
1.

To draw the grid lines, first get the intersection of the lines and the clipped cells Polygon and pClipTopo, if
present. Draw only the part of the grid lines that fall within the Polygon.
[VC++]

if (pClipTopo != NULL)
{
pClipTopo->Intersect((IGeometryPtr)m_ipPolyline, esriGeometry1Dimension, &ipClippedLine);
}
if (ipClippedLine == NULL)
ipClippedLine = m_ipPolyline;
...
pDisplay->DrawPolyline(ipClippedLine);
2.

Draw the grid border, ipBorder, by asking it to draw itself.


[VC++]

ipBorder->Draw(pDisplay, ipClippedCellsBoundary, 0);


At this stage, you should also draw any tick marks if your grid requires them.
After the grid lines, you should display the border and labels of the grid.
3.

Draw the labels.


a) If the clip geometry is not specified, draw the labels in the conventional way.
b) If the clip geometry is specified, then determine the location of the labels.

Using the boundary of the grid from step 5 (ipClippedCellsBoundary), iterate each of its Segments.
[VC++]

ISegmentCollectionPtr ipSegCol(ipClippedCellsBoundary);

137

long lSegs;
ipSegCol->get_SegmentCount(&lSegs);
...
ISegmentPtr ipSeg;
for (l = 0; l < lSegs; ++l)
{
ipSegCol->get_Segment(l, &ipSeg);

For each Segment, determine its orientation (horizontal or vertical), by examining the x and y coordinates
of its end points.
[VC++]

IPointPtr fromPt, toPt;


ipSeg->get_FromPoint(&fromPt);
ipSeg->get_ToPoint(&toPt);
double fx, fy, tx, ty;
fromPt->QueryCoords(&fx, &fy);
toPt->QueryCoords(&tx, &ty);
if ( fabs(ty - fy) < 0.0001 )

//Y coordinates match = horizontal

Next, determine the label's position relative to the Segment. In this example, a test Point is created on top
of a horizontal Segment.
[VC++]

testPt->PutCoords( ((tx + fx) / 2.0), ty + (yInterval / 2.0) );


ipRelClippedCells->Contains((IGeometryPtr)testPt, &bContains);

If testPt is contained by the clip geometry, then the label is known to be at the bottom of the grid. If
testPt is not contained by the clip geometry, you need to place the label above a horizontal Segment or to
the right of a vertical Segment.

Due to the irregular shape of the ClippableIndexGrid, you will need to work out the positioning
of each grid label.

At this point you should also check the value of the label visibility properties (these are retrieved at the
start of the DisplayGrid function). For example, as shown in the code below, only draw the top axis labels
if the property indicates they should be visible.
[VC++]

if (bContains == VARIANT_FALSE && labelTopVis)


{
...
Check whether labels should be visible before drawing them by using the
IMapGrid::QueryLabelVisibility method.

Calculate the correct label text based on the current row and column of the grid, then draw the label. Pass
in to the Draw method of the GridLabel the leftmost point of the Segment (this will be the FromPoint of
the Segment if it is above the grid, and the ToPoint if it is below the grid), and also the appropriate
esriGridAxisEnum constant to indicate the label's relative position to the Point.
[VC++]

ipTabStyle->PrepareDraw(_bstr_t(label), xInterval, corner);


...
ipLabelFormat->Draw(tx, toPt, esriGridAxisBottom, pDisplay);
Calculate a map grid label's text from the current grid row and column numbers.
The example code shows one way of solving the problems of drawing a complex grid. There are, of course, a variety of
different approaches to each of the logic issues encountered.
The Draw method
The Draw method is called by ArcMap when a page layout is refreshed.
For the ClippableIndexGrid, Draw simply calls the DisplayGrid function, passing in the references to IDisplay and
IMapFrame it receives. In the discussion of the DisplayGrid function above, it is assumed that DisplayGrid is called
from the Draw method.

138

The GenerateGraphics method


The GenerateGraphics method may be called to create a GroupElement representing the grid.
GenerateGraphics also calls DisplayGrid, but first creates a GroupElement to pass in; this signifies to DisplayGrid that
it should create and add graphic elements to the GroupElement, rather than drawing the shapes directly to the
Display.
In the GenerateGraphics method, you need to create a GroupElement containing other graphic
elements representing the individual elements of a map grid.
1.

The GenerateGraphics method receives a GraphicsContainer parameter; use this to get the current Display.
[VC++]

IActiveViewPtr ipActiveView(pGraphicsContainer);
IScreenDisplayPtr ipDisplay;
if (ipActiveView)
ipActiveView->get_ScreenDisplay(&ipDisplay);
2.

The Display will not be drawing at this point (as this method is called from the Convert To Graphics button, as
discussed), therefore, you must call StartDrawing on this display to prepare the device for drawing.
[VC++]

OLE_HANDLE hDC;
ipDisplay->get_hDC(&hDC);
ipDisplay->StartDrawing(hDC, esriNoScreenCache);
3.

Create a GroupElement to contain all the graphic elements that will compose the grid, then call DisplayGrid,
passing in this GroupElement.
[VC++]

IGroupElementPtr ipGroupElement(CLSID_GroupElement);
HRESULT hr = DisplayGrid(ipDisplay, pMapFrame, ipGroupElement);
4.

Tidy up by calling FinishDrawing on the Display. Also, add the GroupElement (now full of the graphic elements
that compose a ClippableIndexGrid) to the GraphicsContainer.
[VC++]

ipDisplay->FinishDrawing();
pGraphicsContainer->AddElement(IElementPtr(ipGroupElement), 0);
If your code calls StartDrawing, you must ensure you call FinishDrawing when you have
finished drawing to a Display.
You can find the full details of how DisplayGrid creates graphic elements for the GenerateGraphics method in the
accompanying example.
Implementing IIndexGrid
As ClippableIndexGrid is a type of IndexGrid, the IIndexGrid interface is implemented and most members are
delegated directly to the contained IndexGrid coclass. IIndexGrid inherits from IMapGrid, which has been previously
discussed.
If you are adapting this sample to create a different type of custom map grid, consider implementing IIndexGrid if
your grid will divide the dataframe into equal sections and if part of your adaptation involves the specific members of
IIndexGrid.
For example, you may want to perform spatial operations on the extent of standard grid cells, as demonstrated in this
example using the QueryCellExtent method. You may also want to provide access for clients to set each column and
row label themselves, via the XLabel and YLabel properties.
IIndexGrid provides access to properties, allowing users to set label text individually.
Below is a table describing the typical actions you should perform for each member of IndexGrid; this table contains
only members that are not inherited from IMapGrid.
IIndexGrid members and descriptions
ColumnCount

Return or set the number of columns in the index grid.

QueryCellExtent

Return the cell extent in page space for the given row and column.

RowCount

Return or set the number of rows in the index grid.

XLabel

Allow read-write access to an array of strings, which you should use as the labels for the
columns of the index grid.

YLabel

Allow read-write access to an array of strings, which you should use as the labels for the rows of
the index grid.

Creating and Implementing IClippableIndexGrid


Your ClippableIndexGrid needs two more things: you must be able to uniquely identify this class from other grids when
programming, and you must also provide a way to specify the clip geometry for the grid.

139

You can achieve both these goals by creating and implementing the IClippableIndexGrid interface.
The basic shape of the clippable index grid will be set via a new interface, IClippableIndexGrid.
The read-write IndexGrid property exposes the IndexGrid contained by your ClippableIndexGrid for convenienceits
IClone interface can be used externally for operations such as cloning and checking for equality. In normal operation,
the contained IndexGrid referenced by this member is set at the start of the ClippableIndexGrid's constructor.
The read-write ClipGeometry property simply holds the shape of the gridthe key to the ClippableIndexGrid's shape.
[VC++]

STDMETHODIMP CClippableIndexGrid::put_ClipGeometry(IGeometry *newVal)


{
if (newVal == NULL)
{
m_ipClipGeometry = NULL;
return S_OK;
}
IClonePtr ipNew(newVal);
IClonePtr ipClone;
ipNew->Clone(&ipClone);
m_ipClipGeometry = ipNew;
return S_OK;
}
This geometry, which is set in map units, is used as the base for the grid; only the cells of this base grid that overlap
the geometry are included as part of the final grid.
The value of the ClipGeometry property will be set in two circumstances: when a ClippableIndexGrid is created by the
NewClippableIndexGrid property page and when the ClipGeometry is reset by the ClippableIndexGrid property page.
You will construct both these property pages in the 'Plugging your custom grid into ArcMap' section below.
The ClipGeometry property is the key to the functionality of the ClippableIndexGrid. This property
will be set via the user interface using the property pages you will create later.
Implementing IGraphicsComposite
The presence of IGraphicsComposite indicates that a class is a composite of other graphic elements. It also allows
clients to access those elements.
The IMapGrid::GenerateGraphics method also returns the grid in graphic element form. However, IGraphicsComposite
is a generic interface, implemented by many ArcObjects coclasses, allowing clients to work at this generic level.
As the client should not be able to change the composite parts of the grid, IGraphicsComposite only needs to return a
copy of the elements that compose the grid. This is achieved by the Graphics property, which returns an element
enumeratora class that implements IEnumElement. Neither of the standard classes that implement IEnumElement
can be used in this case, as you cannot add elements to these classes. Therefore, you will need to create your own
enumerator classsee the 'Creating an element enumerator' section below.
IGraphicsComposite is a generic interface, which returns an enumeration of graphic elements. None
of the existing element enumerators are suitable for this job, so you will create a new enumerator
coclass.
As the IGraphicsComposite interface is designed to be generic, the second parameter passed to the Graphics property
is an IUnknown reference, pData; the expected coclass of this parameter will vary according to the implementing
class. A map grid class would expect pData to be a reference to the Map with which the grid is associated.
[VC++]

IMapFramePtr ipMapFrame = pData;


if (ipMapFrame == NULL)
return E_INVALIDARG;
The Graphics method also receives an IDisplay reference, pDisplay, indicating the Display for which the graphics
should be created. Call the StartDrawing method of the Display to prepare it for drawing.
[VC++]

if (FAILED(hr = pDisplay->StartDrawing(0, esriNoScreenCache)))


return hr;
You can make use of the DisplayGrid function again to create the actual graphic elements. Create a GroupElement,
then call the DisplayGrid function, passing in a reference to this GroupElement. This signifies to DisplayGrid that it
should create and add graphic elements to the GroupElement instead of drawing to the display.
[VC++]

IGroupElementPtr ipGroupElement(CLSID_GroupElement);
if (FAILED(hr = DisplayGrid(pDisplay, ipMapFrame, ipGroupElement)))
return hr;
After the function returns, finish drawing on the display.
[VC++]

140

pDisplay->FinishDrawing();
Finish by creating an EnumElement and adding the GroupElement to the enumerator via the IEnumElementAdmin
interface.
[VC++]

IEnumElementAdminPtr ipEnumElementAdmin;
if (FAILED(hr = ipEnumElementAdmin.CreateInstance(CLSID_EnumElement)))
return hr;
IElementPtr ipElem = ipGroupElement;
if (FAILED(hr = ipEnumElementAdmin->Add(ipElem)))
return hr;
Return the EnumElement from the Graphics property.
Use the DisplayGrid function to fill a GroupElement with graphic elements representing the map grid.
Then add each individual element from the GroupElement to an EnumElement to return the Graphics
property element enumeration.
At ArcGIS 9, ArcMap does not call IGraphicsComposite::GetGraphics, but you should implement it to ensure correct
operation of your map grid with future or alternative clients.
Creating an element enumerator
As you cannot add specific elements to the existing element enumerators, ElementSelection and
SimpleElementSelection, you should create a new enumerator class, named EnumElements, to return an enumeration
from the IGraphicsComposite::Graphics method.
Also, create an interface called IEnumElementAdmin with a single method called Add that takes an IElement
parameter. Implementing this interface on EnumElement will allow you to add elements to your element enumerator.

Creating the EnumElement class and IEnumElementAdmin interface help you to implement
IGraphicsComposite.
To store the elements in the enumerator, declare a member variable as an Array; add another member variable to
store the current array position of the enumerator.
[VC++]

IArrayPtr m_pElements;
long

m_lPosition;

In the IEnumElementAdmin::Add method, add a reference to pElement to the last position of the array.
[VC++]

long lCount;
m_pElements->get_Count(&lCount);
IUnknownPtr ipUnk = pElement;
return m_pElements->Insert(lCount, ipUnk);
Finish the EnumElement class by implementing IEnumElement, as shown in the accompanying source code. More
information on creating enumerators can be found in Chapter 2, 'Developing Objects'.
Implementing IClone, IPersist, and IPersistStream
Cloning and persistence are essential functions for plugging any map grid into the ArcGIS system. For example, each
time a map grid's property sheet is displayed, the map grid will be cloned. Persistence is essential to allow your grid to
be saved to and loaded from a map document.
The ClippableIndexGrid example provides a standard implementation of the IClone, IPersist, and IPersistStream
interfaces.
A map grid must implement the standard cloning and persistence interfaces.
In the implementation of IPersist, the clip geometry, m_ipClipGeometry, and contained IndexGrid, m_ipIndexGrid, are
persisted to the stream's ObjectStream. The vector arrays of label strings, m_xLabels and m_yLabels, are persisted as
individual strings by first saving the number of string elements.
See Chapter 2, 'Developing Objects', for more information on cloning and persistence.

Implementing other kinds of custom grids


If you are designing a different kind of map grid, you may also want to implement the IProjectedGrid, IMeasuredGrid,
or ICustomOverlayGrid interfaces depending on the design of the grid.
IMeasuredGrid

141

Consider implementing this interface if your grid is designed to follow a coordinate system. Measured grids have an
origin, and grid lines are drawn at fixed distance intervals.

IMeasuredGrid members and descriptions


FixedOrigin

Return or set a value indicating if the grid should take its origin from the XOrigin and YOrigin
properties (true) or if it is computed dynamically from the data frame (false).

Units

Return or set a constant indicating the units for the intervals and origin.

XIntervalSize

Return or set the interval between grid lines along the x axis.

XOrigin

Return or set the origin of the grid on the x axis.

YIntervalSize

Return or set the interval between grid lines along the y axis.

YOrigin

Return or set the origin of the grid on the y axis.

IProjectedGrid
Consider implementing the IProjectedGrid interface if you will be exposing a spatial reference for your grid. This
interface has a single member, SpatialReference, indicating the coordinate system of the grid. This member should be
coded to allow an ISpatialReference object to be read or written by reference.
ICustomOverlayGrid
You may want to implement this interface if your grid will be based on the Features of an existing FeatureClass, and
your grid label text is stored as attributes of those Features.
ICustomOverlayGrid members and descriptions
IDataSource

Return or set an IFeatureClass reference, indicating the data source of the grid lines.

LabelField

Return or set a string indicating the name of the Field in the data source that should be used to
label the map grid.

Plugging your custom grid into ArcMap


Now that you have created your custom map grid coclass, the next step is to enable a user to create a new
ClippableIndexGrid within ArcMap and edit the grid's properties. In ArcMap, a user may create a new instance of an
existing grid in one of three ways.
First, a user may create a grid by opening the Data Frame properties dialog box, clicking the Grids property page, and
clicking New Grid. This has one of two actions.
If the ArcMap 'Use wizards if available' option is selected, the Grids and Graticules wizard is displayed. This allows the
user to select the type and properties of the new grid. However, this action cannot be extended to work with your
custom grid, as the wizard is hard coded.
If the 'Use wizards if available' option is not selected, the Reference System Selector dialog box is displayed instead,
allowing you to select a predefined grid from those stored in the StyleGalleries and to edit the details of a grid by
clicking Properties.

If you have previously stored a ClippableIndexGrid StyleItem in a referenced StyleGallery, then you will be able to
select this grid and alter its properties. However, the dialog box does not allow you to create a new ClippableIndexGrid
from scratch.

142

If you do want to provide a way to create a new ClippableIndexGrid from the Grid's property page, see the section
Creating the NewClippableGridPage.
Alternatively, a user can create a grid, either based on an existing grid in a StyleGallery or from scratch, by using the
Style Manager dialog box.
To open the Style Manager in ArcMap, click Tools, Styles, then Style Manager. To create a new grid, click the
Reference Systems folder. Then, to create a grid based on an existing StyleItem, click an existing grid. Alternatively,
to create a new grid from scratch based on a grid type, right-click the left-hand pane, and click New from the context
menu.
This list of options for a new grid is taken from the MapGridFactory classes currently registered to the ESRI Map Grid
Factories component category.
So, to allow user access to create a new ClippableIndexGrid, you will now create an accompanying grid factory object
class.

Creating a ClippableIndexGridFactory

By reviewing the ArcMap object model diagram, you can see that the existing map grid factories inherit from the
abstract MapGridFactory abstract class and implement only one interfaceIMapGridFactory.
To solve the requirements of this example, you will create a class that is a subtype of MapGridFactory called
ClippableIndexGridFactory.
Once the ClippableIndexGridFactory is registered to the ESRI Map Grid Factories component category, a user will be
able to create a new ClippableIndexGrid from the Style Manager dialog box.
Create a map grid factory to allow users to create new ClippableIndexGrids in the Style Manager.
Implementing IMapGridFactory
IMapGridFactory has one property and one method. The read-only Name property should return the name of the type
of grid the factory creates. In this example it returns "Clippable Index Grid".
Once your custom map grid is built and registered, you will see this name on the context menu when you attempt to
create a new grid in the Style Manager dialog box.
In the Create method, you should create a new instance of the ClippableIndexGrid coclass and call the
IMapGrid::SetDefaults method to set the default properties of the MapGrid. Then return this new grid to the caller.
[VC++]

STDMETHODIMP CClippableIndexGridFactory::Create(IMapFrame *MapFrame, IMapGrid **MapGrid)


{
if (!MapGrid)
return E_POINTER;
*MapGrid = NULL;
IMapGridPtr ipGrid(CLSID_ClippableIndexGrid);
ipGrid->SetDefaults(MapFrame);
*MapGrid = ipGrid;
(*MapGrid)->AddRef();
return S_OK;

143

}
Create will be called when the user selects ClippableIndexGrid from the new grid context menu in the Style Manager
dialog box.
The ClippableIndexGridFactory creates a new ClippableIndexGrid in its IMapGridFactory::Create
method.

Creating a property page for the ClippableIndexGrid


When you attempt to edit the properties of any map grid, the Reference System dialog box appears. When this dialog
box is displayed, it will interrogate all the property pages currently registered to the ESRI Map Grids Property Pages
component category and will display all those pages that apply to the type of map grid being edited.
As your custom map grid class implements IMapGrid, the dialog box will contain the existing Axes, Labels, and Lines
property pages (IPropertyPageContext::Applies for these pages will return True if passed any class that implements
IMapGrid.)
The ClippableIndexGrid also implements IIndexGrid; therefore, the Index property page will also be displayed.
At this point, a user will be able to change all the properties of a ClippableIndexGrid, except
IClippableIndexGrid::ClipGeometrythe one property that is not available via the existing property pages.
Creating the ClippableGridPage

Add to your project a simple property page to allow users to set the clip geometry of a ClippableIndexGrid. Add to the
dialog box a button called Use Selected Data Graphic, allowing the user to set the value of the clip geometry equal to
the geometry of the currently selected graphic element.

Once you have registered this property page to the ESRI Map Grids Property Pages component category, it will appear
in the Reference System dialog box when the user has selected a ClippableIndexGrid.
Implementing IPropertyPage for the ClippableGridPage
ClippableGridPage is a standard implementation of a property page. See 'Creating Property Pages' in Chapter 2 for
more information on implementing a property page.
In the Applies method, iterate through the objects referenced by the SafeArray parameter and return True if you find
an object that implements IIndexGrid and IClippableIndexGrid.
[VC++]

*Applies = VARIANT_FALSE;
long lNumElements = saArray->rgsabound->cElements;
for (long i = 0; i < lNumElements; i++)
{
IClippableIndexGridPtr ipInd(pUnk[i]);
if (ipInd != 0)
{
*Applies = VARIANT_TRUE;
m_ipGrid = ipInd;
break;
}
}
In the SetObjects method, check the array of objects passed in. You should receive a Map and ClippableIndexGrid,
which should be stored as member variables.
[VC++]

STDMETHODIMP CClippableGridPage::SetObjects(ULONG nObjects, IUnknown *ppUnk)


{
for (ULONG i=0; i < nObjects; i ++)

144

{
IMapPtr ipMap(ppUnk[i]);
if (ipMap != 0)
m_ipMap = ipMap;
IClippableIndexGridPtr ipGrid(ppUnk[i]);
if (ipGrid != NULL)
m_ipGrid = ipGrid;
}
...
IPropertyPage::SetObjects should receive a reference to a Map and a reference to a
ClippableIndexGrid.
In response to the user clicking the Use Selected Data Graphic button on the property page, retrieve the graphic
element that is currently selected on the Map you received in SetObjects.
[VC++]

IViewManagerPtr ipViewManager(m_ipMap);
ISelectionPtr ipSelection;
ipViewManager->get_ElementSelection(&ipSelection);
IEnumElementPtr ipEnumElement(ipSelection);
ipEnumElement->Reset();
IElementPtr ipElement;
ipEnumElement->Next(&ipElement);
The Use Selected Data Graphic button allows the user to set the shape of the ClippableIndexGrid.
As the clip geometry must be a Polygon, check the type of this graphic element. Then set the ClipGeometry property
of the ClippableIndexGrid you received in SetObjects to the Geometry of the graphic element.
[VC++]

...
IGeometryPtr ipGeometry;
ipElement->get_Geometry(&ipGeometry);
esriGeometryType type;
ipGeometry->get_GeometryType(&type);
if (type != esriGeometryPolygon)
{
:MessageBoxW(0, L"Clip geometry was not a polygon.", L"ClippableGrid", MB_OK);
return 0;
}
m_ipClipGeometry = ipGeometry;
Set the IClippableIndexGrid::ClipGeometry property from the ElementSelection of the Map.

A User Interface for creating new custom map grids


Previously, you saw you cannot create a new custom map grid from the Data Frame Properties dialog box via the Grids
and Graticules wizard.
The Grids and Graticules wizard is not extensibleyou cannot add your custom grid to this wizard.
You can still allow the creation of a new custom map grid from the Data Frame Properties dialog box by adding a new
property page that has this functionality to the dialog box.
Although this is a slightly nonstandard way to extend the framework, this technique does show you the flexibility of
property pages used in conjunction with component categories.
Creating the NewClippableGridPage

You will create a simple property page, NewClippableGridPage, to allow users to add a new ClippableIndexGrid to a
Map. This property page will appear in the Data Frame Properties dialog box, as you will register it to the ESRI Map
Property Pages component category.
The NewClippableGridPage property page is shown hereit is a standard implementation of a property page (again,
see the 'Creating Property Pages' section in Chapter 2).

145

The check box at the top is unchecked by default. When checked, it enables the remainder of the dialog box's controls.
You can set a name for the grid and change the number of columns and rows in the grid. These changes are stored as
simple member variables while the page is displayed.
There is also a dropdown list box that allows you to choose from a number of options for the tab style of the labels for
the grid; these are hard-coded in this example, but could be identified at run time from the Grid Labels component
category. This selection is also stored as a member variable.
Last, there is also a button that allows you to set the clip geometry of the ClippableIndexGrid to equal the currently
selected graphic. The code behind this button is similar to that shown for the ClippableGridPage previouslythe
geometry of the graphic is stored as a member variable.
[VC++]

IViewManagerPtr ipViewManager(m_ipMap);
ISelectionPtr ipSelection;
ipViewManager->get_ElementSelection(&ipSelection);
IEnumElementPtr ipEnumElement(ipSelection);
ipEnumElement->Reset();
IElementPtr ipElement;
ipEnumElement->Next(&ipElement);
IGeometryPtr ipGeometry;
ipElement->get_Geometry(&ipGeometry);
m_ipGeometry = ipGeometry;
Implementing IPropertyPage for the NewClippableIndexGrid
In the Applies method, instead of iterating through the array passed in and checking for a particular type of object,
simply return True. You want the NewClippableGridPage to always appear in the Data Frame Properties dialog box,
regardless of the properties.
In the SetObjects method, check the array of objects passed inyou should receive a reference to a Map. Store this
reference as a member variable; you will add your grid to this Map later in the Apply method.
[VC++]

for (ULONG i=0; i < nObjects; i ++)


{
IMapPtr ipMap(ppUnk[i]);
if (ipMap != 0)
m_ipMap = ipMap;
}
IPropertyPage::SetObjects should receive a reference to a Map.
The majority of the work done by the NewClippableGridPage is in the Apply method. First, instantiate a new
ClippableIndexGrid.
[VC++]

IClippableIndexGridPtr ipClippedGrid(CLSID_ClippableIndexGrid);
IIndexGridPtr ipGrid(ipClippedGrid);
Next, set the values of its Name, Rows, and Columns properties from the member variables you stored previously.
[VC++]

TCHAR sText[100];
::GetWindowText(m_hEdtName, sText, 100);

146

_bstr_t bsName = sText;


ipGrid->put_Name(bsName);
...
Then, set the IIndexGrid::TabStyle property by instantiating the correct type of tab style class based on the style
selected by the user.
[VC++]

::GetWindowText(m_hCboTabType, sText, 100);


_bstr_t bsTabStyle = sText;
IIndexGridTabStylePtr ipTabStyle;
if (bsTabStyle == _bstr_t(L"Button Tabs"))
{
ipTabStyle.CreateInstance(CLSID_ButtonTabStyle);
}
else if (bsTabStyle == _bstr_t(L"Filled Background"))
{
...
Apply default values for the color and thickness of the tab, then set the IIndexGrid::TabStyle property of the
ClippableIndexGrid.
[VC++]

IRgbColorPtr color(CLSID_RgbColor);
color->put_Red(255);
color->put_Blue(190);
color->put_Green(190);
ipTabStyle->put_ForegroundColor((IColorPtr)color);
color->put_Blue(110);
color->put_Green(110);
color->put_Red(110);
ipTabStyle->put_OutlineColor((IColorPtr)color);
[VC++]

ipTabStyle->put_Thickness(20.0);
ipGrid->put_LabelFormat((IGridLabelPtr)ipTabStyle);
In the IPropertyPage::Apply method, create the new ClippableIndexGrid and set its properties
according to the selections made by the user on the NewClippableIndexGrid property page.
Don't forget to set the IClippableIndexGrid::ClipGeometry property.
[VC++]

ipClippedGrid->put_ClipGeometry(m_ipGeometry);
Now you need to add the ClippableIndexGrid to the Map. Start by getting the GraphicsContainer of the PageLayout,
and from this find the FrameElement of the Map.
[VC++]

IApplicationPtr ipApp(CLSID_AppRef);
IDocumentPtr ipDoc;
ipApp->get_Document(&ipDoc);
IMxDocumentPtr ipMxDoc(ipDoc);
IPageLayoutPtr ipPageLayout;
ipMxDoc->get_PageLayout(&ipPageLayout);
IGraphicsContainerPtr ipGC(ipPageLayout);
IFrameElementPtr ipFrame;
ipGC->FindFrame(_variant_t((IUnknown*)m_ipMap), &ipFrame);
The Apply method should also add the new ClippableIndexGrid to the MapFrame.
Note that the code here assumes it is running inside the ArcMap process and uses the AppRef object. If there is a
chance that your property page may be used outside ArcMap, using AppRef may cause errors. You may want to refer
to Chapter 2, 'Developing Objects', for information on a technique to avoid the instantiation of AppRef outside the
ArcGIS applications.
Using AppRef may cause errors if your code finds itself running in a process outside ArcMap.
Finally, add the ClippableIndexGrid and refresh the view to show your new grid.
[VC++]

IMapGridsPtr ipMapGrids(ipFrame);
ipMapGrids->AddMapGrid((IMapGridPtr)ipGrid);
IActiveViewPtr ipAV(ipPageLayout);
ipAV->PartialRefresh(esriViewBackground, NULL, NULL);

147

CreateCompatibleObject and QueryObject are not applicable methods in this context, as the grid property pages are
mutually exclusiveso return E_NOTIMPL.
Once compiled and registered, your clippable index grid is ready for use.
Go to example code
See Also About Map Grids and Creating Cartography.

Layer Classes in ArcGIS


The ArcGIS libraries define many different types of layer classes to visually represent different sources of data (for
example, FeatureLayer, TinLayer, CadLayer, and CoverageAnnotationLayer). These layers display geographic data
stored in datasets, such as shapefiles, CAD files, image files, and feature classes, which are stored in a geodatabase.
Look at the Carto Layer Object Model Diagram to find out more about the existing layer classes. To begin, you can see
that all layer objects are subtypes of the Layer abstract class. All layer objects implement the interfaces ILayer and
IGeoDataset; therefore, any custom layer should implement at least these interfaces. The ILayer interface controls the
drawing properties and actual drawing of the layer. The IGeoDataset interface defines the extent and spatial reference
system for a layer so that it can be projected and georeferenced.
When you look further at the Carto Layer OMD, you will see that the class hierarchy for layers appears somewhat
complex. This is because there are a wide range of different types of layers providing wide ranging functionality. You
will find that the FeatureLayer class inherits from not only the Layer abstract class but also from the DataLater,
DisplayLayer, TableLayer, and AnalysisLayer abstract classes. The RasterLayer inherits from all these except the
AnalysisLayer abstract class because AnalysisLayer contains interfaces that work with a vector-based data model,
which uses features (for example, IFeatureSelection); a raster layer, of course, does not use a vector data model.
Many other classes that derive from these layer abstract classes can be found, via the indicated links on the diagrams
of other areas of the ArcGIS object model.

Creating Custom Layers


You may have a custom or unsupported data format that you would like to display in a map without having to first
convert it to a data format supported by ArcGIS. Perhaps you would to like to extend the way an existing layer class
draws? Writing your own layer object enables you to support the drawing of new data formats and to customize how
existing data formats are displayed.
A custom layer allows you to display an unsupported data source in a map. You could also change
the way an existing layer class draws by using a custom layer.
If you have a custom data format you would like to support in ArcGIS there are a few different options you can
implement. See 'Plug-in data sources' in Chapter 7 for more information on the different solutions for integrating
custom data sources, in particular the table summarizing the benefits of each solution.
Custom layers are often used to accentuate a map so better spatial analysis and edits can be made to feature datasets
already loaded in the map. Like annotation layers, custom layers can be used to display different geometric objects
such as points and lines in a single layer. Custom layers can be used to display dynamic data as well. If your objective
is to simply map your data by providing a custom visual representation and there are not any requirements to provide
functionality, such as complex editing and data analysis, then a custom layer object is likely an excellent solution for
you. Otherwise, you should consider the option of importing your data into a format supported by ArcGIS so you can
take advantage of the many toolsets provided to work with your data.
If you would like to extend the way ArcGIS draws the currently supported data formats, there are a few different
options you can implement. See 'Custom feature renderers' in Chapter 5 of Extending ArcObjects for more
information. Extending the way an existing layer coclass (for example, FeatureLayer) draws requires the creation of a
custom layer that aggregates this ArcGIS layer coclass. COM containment of the ILayer interface implementation
would be required so that the Draw method could be overridden.
All layers must implement ILayer. For ArcMap to save a custom layer, the interface IPersistStream must be
implemented. (Visual Basic developers need to implement IPersistVariant instead.) You may choose what custom layer
properties to persist in addition to the ILayer member properties such as the minimum and maximum scales, the
name of the layer and its visible state. For a layer to be included in the ArcMap table of contents window, it must
implement the ILegendInfo interface. The implementation of the interfaces ILayer, IGeoDataset, IPersistStream and
ILegendInfo will provide a basic level of integration with the ArcGIS framework.
To help you create a typical custom layer, an example is presented of a SimplePointLayer, which inherits from the
Layer abstract class and also implements certain other typical layer interfaces. To keep the example simple enough to
follow and understand, not all the numerous possible interfaces are implemented in the example.
See Also Creating Cartography and Simple Point Layer Example.

148

Simple Point Layer Example


Object Model Diagram

Example Code Click here


Description This project provides a custom layer that reads geographic data from a file-based data source.
Supporting classes include a property page that allows the user to change the source file for the layer and a custom
IdentifyObject to identify the feature attributes for the layer. To further complete the custom layer implementation, a
custom GxObject is provided so that the data file can be browsed and previewed in ArcCatalog. A layer factory, name,
and layer enumeration are also provided to allow you to preview the layer in ArcCatalog.
The VB example project is restricted in scope. As the ILayerFactory and IName interfaces cannot be implemented in
VB6, the VB sample provides the custom layer, identify object, and property page onlythis means that the VB
custom layer must be added programmatically to a map.
Design SimplePointLayer is a subtype of the Layer abstract class. The coclass SimplePointLayerPropPage is a property
page that applies to the layer object. A custom IdentifyObj object, SimplePointLayerIdentifyObj, also accompanies this
layer to enable feature identification.
SimplePointLayerGxObject is a subtype of the GxObject class, which represents the data in ArcCatalog.
SimplePointLayerGxObjectFactory is a subtype of the GxObjectFactory class and is responsible for creating this custom
GxObject.
SimplePointLayerFactory is a subtype of the abstract class LayerFactory. The LayerFactory creates the layer by first
evaluating the layer's associated Name object, SimplePointLayerName. When the LayerFactory creates the layer, it
adds it to an enumeration, SimplePointEnumLayer.
License ArcView or above.
Libraries Carto, Catalog, Display, Framework, GeoDatabase, Geometry, System, and SystemUI.
Languages Visual Basic (some restrictions), Visual C++.
Categories ESRI Layer Property Pages, Layer Factory, and ESRI GxObject Factories.
Interfaces (VC++) IEnumLayer, IGeoDataset, IGxLayerSource, IGxObject, IGxObjectFactory,
IGxObjectFactoryFileExtensions, IGxObjectUI, IIdentifyObj, ILayer, ILayerInfo, ILayerDrawingProperties,
ILayerFactory, ILegendInfo, IIdentify, IName, IPersistStream, IPropertyPage, IPropertyPageContext.
(VB) ILayer, IGeoDataset, IIdentify, ILayerInfo, ILegendInfo, ILayerDrawingProperties, IPersistVariant, IIdentifyObj,
IComPropertyPage
How to use
VC++
1.

Open and build the workspace SimplePointLayerVC.dsw, to register SimplePointLayerVC.dll and


GxSimplePointLayerVC.dll and to register to component categories.

149

2.

Open ArcMap.

3.

Click the Add Data command and browse for the simple point file. Click Open to add the new layer to the
map.

4.

Right-click the layer in the table of contents window and click Properties, then click the 'Simple Point Layer'
property page to change the data source file for the layer.

5.

Use the Identify tool and click on a point in the layer to display its attribute.

6.

Now open ArcCatalog.

7.

Traverse the TOC for the file and use the Geographic Preview window to view the data.

1.

Register SimplePointLayerVB.dll and double-click the SimplePointLayerPropPage.reg file to register to


component categories.

2.

Open ArcMap.

3.

Add a layer to ArcMap using this VBA Macro. (Remember to add a reference to SimplePointLayerVB.dll
from your VBA project.

VB6

[Visual Basic]

Sub AddSimplePointLayer()
Dim pLPT As SimplePointLayerVB.ISimplePointLayer
Set pLPT = New SimplePointLayerVB.SimplePointLayer
pLPT.File = <path to data>
Dim pLyr As esriCarto.ILayer
Set pLyr = pLPT
If pLyr.Valid = False Then
MsgBox "please check path to data"
Exit Sub
End If
Dim pMxDoc As esriArcMapUI.IMxDocument
Set pMxDoc = Application.Document
pMxDoc.AddLayer pLyr
End Sub
4.

Right-click the layer in the table of contents window and click Properties, then click the 'Simple Point Layer'
property page to change the data source file for the layer.

5.

Use the Identify tool and click on a point in the layer to display its attribute.

The case for a custom simple point layer


Many different formats of data are supported for viewing in ArcMap as layers: coverages, CAD files, personal
geodatabase feature classes, and so on. In addition to these layers, you can use the Add XY Data command in ArcMap
to display a Table as a layer. This means that you can take a table that contains x and y coordinates and display this
as if it were a point layer.

150

If you have data stored in an ASCII file, it may not always be possible to use this as a layer however. For example, if
your ASCII file uses a fixed-width format instead of a delimited format, you will not be able to use this as a table and,
therefore, will not be able to display this as a layer.

Therefore, for your fixed-width ASCII x,y data, you may want to create a custom layer to display the data as a layer,
rather than performing some kind of data conversion on the data files.

Creating the SimplePointLayer

To solve the requirements of this example, you will create a subtype of the Layer abstract class called
SimplePointLayer, by implementing ILayer and IGeoDataset. Implementations of the SimplePointLayer are available as
Visual C++ and Visual Basic sample projectspersistence is added by implementing IPersistVariant in VB, or
IPersistStream in VC++. Throughout the discussion of the sample we will follow the VC++ implementationthe VB
implementation will be discussed where it differs from the main concepts of the VC++ implementation.
In addition to the minimum interfaces, you will implement ILayerDrawingProperties, which is typically used internally
by a layer's property page to indicate if some properties of the layer have been changed so that the layer needs to be
redrawn. To associate an icon with the layer file, you will also implement the ILayerInfo interface. To include the layer
in the ArcMap table of contents window, you will also implement the ILegendInfo interface.
To be able to use the Identify tool on features in the custom layer, you will also implement IIdentify. This
implementation also requires a custom IdentifyObj object, which will be covered in great detail in a separate section
below.
You will also create a property page, which will be discussed in more detail in the Layer Property Page section below.

151

The custom SimplePointLayer will allow an unsupported data format to be displayed by ArcGIS as a
layer.
By reviewing the behavior and implementation details of existing layer coclasses, such as FeatureLayer, RasterLayer,
and CadFeatureLayer, you will see that the inclusion of these interfaces - ILayerInfo, ILayerDrawingProperties and
IIdentify - provides a higher level of integration with the ArcObjects framework. For more information on Layers, look
at the Carto Library Reference Overview in the ArcGIS Developer Help.
Implementing ILayer
The first interface you will implement is ILayer. The implementation of ILayer provides the system the information it
needs to draw the layer.
A layer should be considered invalid if there is a problem connecting to the datasource. If a layer is not valid, then it
should not be drawn nor should its extent be returned or its features identified; for example, in the Draw method you
will check to see that the layer is valid before drawing. Add a member variable to indicate the validity of the layer and
return its value from Valid. You will set this value later in the 'Creating and Implementing ISimplePointLayer' section.
[C++]

STDMETHODIMP CSimplePointLayer::get_Valid(VARIANT_BOOL * Valid)


{
if (Valid == NULL)
return E_POINTER;
*Valid = m_bValid;
return S_OK;
When a new spatial reference is set in the Map, a reference to the new coordinate system is passed to the
SpatialReference member of ILayer. To implement the SpatialReference property, simply store this reference.
[C++]

ISpatialReferencePtr m_ipLayerSpatialRef; // data frame


...
STDMETHODIMP CSimplePointLayer::putref_SpatialReference(ISpatialReference * pSR )
{
m_ipLayerSpatialRef = pSR;
return S_OK;
Your layer will need to apply the current spatial reference when drawing features, identifying features, and returning
its own extent. Remember to project from the data source's native spatial reference system to the spatial reference
system applied to the Map. The datasource's spatial reference system is indicated in the IGeoDataset implementation
of the layer. This interface is discussed in more detail in the Implementing IGeoDataset section below.
When a layer is drawn, the map will ask the layer for its AreaOfInterest property so that it can determine where on the
map to draw. The AreaOfInterest can be set by asking the layer for its full extent. The Extent of the Layer can be
retrieved from its IGeoDataset implementation. You should expand this extent so the symbols used to draw the layer's
features are fully included in the display. This will be discussed in more detail in the Implementing IGeoDataset section
below.
[C++]

STDMETHODIMP CSimplePointLayer::get_AreaOfInterest(IEnvelope * * aoi)


{
if (aoi == NULL)
return E_POINTER;
if (!m_bValid)
{
aoi = 0;
return S_OK;
}
return get_Extent(aoi);
The actual drawing of the layer occurs in the Draw member method.
1.

Drawing should only occur for the draw phase or phases that apply to the layer. In this case the applicable draw
phase is the geography phase.
[C++]

STDMETHODIMP CSimplePointLayer::Draw(esriDrawPhase DrawPhase, IDisplay * Display, ITrackCancel


* trackCancel)
{
if (DrawPhase != esriDPGeography) return S_OK;
..
2.

If a layer is not Valid and Visible, then it should not be drawn, therefore check these members before continuing.
[C++]

152

if (m_bValid == VARIANT_FALSE) return S_OK;


If a layer is not Visible, then it should not be drawn, nor should its extent be returned or its features identified.
For example, in the Draw method, you should check to see that the layer is valid before continuing. A layer
should be considered invalid if there is a problem connecting to the datasource. Indicate whether or not a layer
is valid through its Valid member.

3.

[C++]

if (m_bValid == VARIANT_FALSE) return S_OK;


if (m_bVisible == VARIANT_FALSE) return S_OK;
Note that in addition to an esriDrawPhase constant, an IDisplay reference is passed into the Draw method. You
will use this reference to set the symbol for the geometries into the display and to actually draw the geometries
for your features.
You can obtain the symbol for your layer from its ILegendInfo interface. The implementation of this interface will
be discussed in more detail in the 'Implementing ILegendInfo' section below; in short, when a layer supports
ILegendInfo, it will have a LegendGroup associated with it. Retrieve this collection of legend classes to get the
symbol that will be used to draw the layer.

4.

[C++]

...
ILegendClassPtr ipLegendClass;
ISymbolPtr

ipSym;

m_ipLegendGroup->get_Class(0, &ipLegendClass);
ipLegendClass->get_Symbol(&ipSym);
Display->SetSymbol(ipSym);
..
Now that the symbol has been set to the display, you can draw the geometries for your features. (As discussed
at the beginning of this topic, the data source is a simple ASCII text file containing coordinates and a character
attribute for a point on each line of the file; see the later section called 'Creating and Implementing
ISimplePointLayer' for information about how you will provide functions to connect to the data source and
retrieve the data.) For each line in the file, create a point by calling the method ISimplePointLayer::NextRecord
to retrieve the feature data. Draw each point to the Display specified by the Draw method of the layer. Note that
each point retrieved must be projected from its native spatial reference system to the spatial reference set by
the Map. (Projections will be discussed in more detail below.)

5.

[C++]

...
while (hr != E_FAIL)
{
hr = NextRecord(&ipPt, &bstrAttr);
if (hr != E_FAIL)
{
if (m_ipLayerSpatialRef)
ipPt->Project(m_ipLayerSpatialRef);
Display->DrawPoint(ipPt);
}
}
return S_OK;
Note that the Draw method does not need to consider the layer's MinimumScale and MaximumScale properties when it
draws. The Display will consider the draw scale of the map before it asks the layer to draw itself.
The following table summarizes the members of ILayer that have been discussed and describes the implementation of
the other members that did not require detailed discussion above.
ILayer member

Implementation description

AreaOfInterest

Return an IEnvelope reference storing the area of interest for the layer. The envelope
geometry should have the same spatial reference system as the Map. The AreaOfInterest is
usually the same as the combined extent of the features in the layer.

Cached

Return or set a boolean indicating if the layer should use its own display cache. This is an
informational property and the management of the cache is not done by the layer but by the
display container.

Draw

Draw the layer to the specified display for the appropriate draw phase. You will set the
symbols for the geometries to be drawn, then draw each feature for your layer.

MaximumScale

Return or set the maximum scale (representative fraction) at which the layer will display.

MinimumScale

Return or set the minimum scale (representative fraction) at which the layer will display.

Name

Return or set a string value that indicates the name of the layer.

ShowTips

Return or set a boolean indicating if the layer shows map tips The tip is specified in the

153

TipText property.
SpatialReference

Set an ISpatialReference reference passed by the Map to the layer. The layer will need to
draw its geometries in this spatial reference.

SupportedDrawPhases

Return an esriDrawPhase constant or a combination of esriDrawPhase constants indicating


the draw phases supported by the layer.

TipText

Return a string value indicating the Map tip text for the specified location.

Valid

Return a boolean value indicating if the layer is currently valid. You will need to determine
what situations render your layer invalid.

Visible

Return or set a boolean value indicating if the layer is currently visible.

Implementing IGeoDataset
The information about the spatial reference system and spatial extent for your layer's datasource is managed by the
members of the IGeoDataset interface. This interface must be implemented for the Map to be able to georeference and
project the layer.
The SpatialReference member should return the native spatial reference system for the layer's datasource. In this
project, this property has been set to the world Robinson projection. If the metadata for the spatial reference system
was stored in the datasource, then this information could be retrieved to dynamically set the spatial reference system
for the dataset.
[C++]

STDMETHODIMP CSimplePointLayer::get_SpatialReference(ISpatialReference * * spref)


{
if (spref == NULL)
return E_POINTER;
// indicate the native spatial reference system for the layer.
if (m_ipDataSpatialRef ==0)
{
ISpatialReferenceFactoryPtr

ipSRF(CLSID_SpatialReferenceEnvironment);

IProjectedCoordinateSystemPtr

ipPCS;

ipSRF->CreateProjectedCoordinateSystem(esriSRProjCS_World_Robinson, &ipPCS);
m_ipDataSpatialRef = ipPCS;
}
*spref = m_ipDataSpatialRef;
(*spref)->AddRef();
return S_OK;
The read-only SpatialReference property on IGeoDataset should return the details of the coordinate
system in which the data is stored.
The write-only SpatialReference property on ILayer indicates to the Layer the coordinate system it
should use to draw itself and return its other spatial properties such as Extent.
To complete the implementation of this interface, you will need to specify the spatial extent which contains all the
features of the layer. The extent is usually calculated as the minimum bounding rectangle of the layer; however, you
may need to incorporate the spatial extent of the symbol or symbols used to display the layer's features as wellthis
issue is generally applicable to point data as well as to line data with a thick symbol or polygon data with a thick
outline symbol.

1.

To calculate the extent of the layer, you first need to get the minimum and maximum coordinates in the dataset
by stepping through each line of the datafile. Then use these coordinates to construct an envelope geometry,
assigning the spatial reference to be the same as that of the data source. Create a private function,
GetLayerExtent, to perform this work.
[C++]

HRESULT CSimplePointLayer::GetLayerExtent(IEnvelope** ppEnv)


{

154

double dxMin, dxMax, dyMin, dyMax;


dxMin = 9999999;
dxMax = -9999999;
dyMin = 9999999;
dyMax = -9999999;
HRESULT hr = S_OK;
double x,y = 0.0;
long lLoopCount = 0;
IPointPtr ipPt;
CComBSTR bstrAttr;
while (hr != E_FAIL)
{
hr = NextRecord(&ipPt, &bstrAttr);
if (hr != E_FAIL)
{
ipPt->get_X(&x);
ipPt->get_Y(&y);
if (x < dxMin) dxMin = x;
if (x > dxMax) dxMax = x;
if (y < dyMin) dyMin = y;
if (y > dyMax) dyMax = y;
}
lLoopCount++;
}
// Handle special case of single point in file
// add a small amount, so that we will end up with an envelope rather than a point
if (lLoopCount == 1)
{
double dDelta = 0.01;
if (dxMax != 0)
dDelta = dxMax/1000;
dxMax = dxMax + dDelta;
dyMax = dyMax + dDelta;
}
IEnvelopePtr ipEnv(CLSID_Envelope);
ISpatialReferencePtr ipSR;
get_SpatialReference(&ipSR);
ipEnv->putref_SpatialReference(ipSR);
ipEnv->put_XMin(dxMin);
ipEnv->put_XMax(dxMax);
ipEnv->put_YMin(dyMin);
ipEnv->put_YMax(dyMax);
*ppEnv = ipEnv;
if (*ppEnv)
(*ppEnv)->AddRef();
return S_OK;
2.

To expand the extent to consider the symbol size, you will need to calculate the map distance that is equivalent
to the size of the symbol. Since the map distance will depend on the extent of the map and the size of the
current symbol, a good place to calculate this value is in the Draw method of the layer. Recall that the symbol
for the layer had to be set into the display before the layer features could be drawn. You can use the Display
which is passed in to the Draw method to calculate the map distance that corresponds to the size of the symbol
being used. Use the display transformation to convert between map and device coordinates, and cache the
calculated value so it can be used to determine the extent of the layer.
[C++]

IMarkerSymbolPtr ipMarker(ipSym);
if (ipMarker)
{
double ptsDist, mapDist = 0.0;
IDisplayTransformationPtr ipDT;

155

ipMarker->get_Size(&ptsDist);
Display->get_DisplayTransformation(&ipDT);
ipDT->FromPoints(ptsDist, &mapDist);
m_dblMarkerDist = mapDist; //Cached symbol size value.
3.

Now complete the Extent member. Return a null reference if the layer is not Valid.
[C++]

STDMETHODIMP CSimplePointLayer::get_Extent(IEnvelope * * Extent)


{
if (Extent == NULL)
return E_POINTER;
if (!m_bValid)
{
m_ipExtent = 0;
return S_OK;
4.

Then call this GetLayerExtent function, clone the incoming Envelope, and project the cloned Envelope to the
spatial reference system applied to the Map.
[C++]

...
if (m_ipExtent == 0)
GetLayerExtent(&m_ipExtent);
if (m_ipExtent == 0) return S_OK;
double

w, scaleFactor = 0.0;

IClonePtr

ipClone;

IClonePtr(m_ipExtent)->Clone(&ipClone);
IEnvelopePtr ipEnv(ipClone);
//project extent if map's spatial reference has been set
if (m_ipLayerSpatialRef)
ipEnv->Project(m_ipLayerSpatialRef);
...
5.

At this point you have converted the symbol units to map units, determined the bounds of the layer and set the
envelope bounds to the map's spatial reference system. You can now finish by accounting for the Symbol size
and returning the Extent.
[C++]

...
//expand the extent to consider the size of the symbols
ipEnv->get_Width(&w);
scaleFactor = (w + m_dblMarkerDist)/w;
ipEnv->Expand(scaleFactor, scaleFactor, VARIANT_TRUE);
*Extent = ipEnv;
if (*Extent)
(*Extent)->AddRef();
return S_OK;
The IGeoDataset interface has only two membersExtent and SpatialReferencewhich are read-only properties. Once
this interface has been implemented, ArcGIS applications, such as ArcMap, can zoom to the layer, view the layer with
other datasets, and project the layer into different map coordinate systems.
Implementing ILegendInfo
To see a layer as an item in the table of contents window in ArcMap, the layer must implement ILegendInfo. Every
layer has a LegendGroup which is a collection of the classes used to display the layer. The LegendGroup links the
symbols used for the layer with the table of contents. In other words, edits made to the legend group for the layer are
passed on to the layer so it can redraw itself using the updated symbol or symbols. (Recall that in the layer's Draw
method, the symbol you used to draw the layer's feature geometry is retrieved from the layer's legend group.)
For the SimplePointLayer you will have one legend class.
[C++]

STDMETHODIMP CSimplePointLayer::get_LegendGroup(LONG Index, ILegendGroup * * LegendGroup)


{
...
*LegendGroup = 0;

156

if (Index == 0)
Initialize the LegendClass to a simple marker symbol.
[C++]

if (m_ipLegendGroup ==0)
{
HRESULT hr;
if (FAILED(hr = m_ipLegendGroup.CreateInstance(CLSID_LegendGroup)))
return hr;
m_ipLegendGroup->put_Heading(CComBSTR(_T("")));
m_ipLegendGroup->put_Editable(VARIANT_TRUE);//can change symbol with right-click in TOC
ILegendClassPtr ipLegendClass(CLSID_LegendClass);
ISymbolPtr

ipSym(CLSID_SimpleMarkerSymbol);

ipLegendClass->putref_Symbol(ipSym);
ipLegendClass->put_Label(CComBSTR(_T("")));
m_ipLegendGroup->AddClass(ipLegendClass);
}
*LegendGroup = m_ipLegendGroup;
(*LegendGroup)->AddRef();
}
return S_OK;
A user can now edit the symbol used by the layer by double-clicking the legend item in the table of contents.

The legend group for the layer will be discussed more when the topic of saving a layer is pursued in the next section.
Implementing IPersistStream and IPersistVariant
As the topic Implementing persistence is discussed in great detail in Chapter 2, this section will only discuss what
needs to be saved to the document in order to properly restore the state of the layer.
To save a layer, you will need to persist at least the following:
1.

All the layer's ILayer properties

157

2.

The layer's legend group

3.

Any custom properties

You will need to save the value of every property of the layer's ILayer implementation when the Save member of
IPersistStream (IPersistVariant for Visual Basic users) is called. You must also save the layer's legend group so that
when the layer is loaded again, it can be drawn with the same symbology as when it was last saved to the document.
Any other custom properties that are required to completely save the state of the layer need to be persisted as well.
For example, for your SimplePointLayer, the path to the datasource must be saved since the layer only references its
geographic data and does not actually store it.
[C++]

STDMETHODIMP CSimplePointLayer::Save(IStream * pStm, BOOL fClearDirty)


{
//persist layer data to stream
HRESULT hr;
if (FAILED(hr = pStm->Write(&cCurVers, sizeof(cCurVers), 0)))
return E_FAIL;
// ILayer members
m_bstrName.WriteToStream(pStm);
pStm->Write(&m_bVisible, sizeof(m_bVisible), 0);
pStm->Write(&m_bCached, sizeof(m_bCached), 0);
pStm->Write(&m_dblMinScale, sizeof(m_dblMinScale),0);
pStm->Write(&m_dblMaxScale, sizeof(m_dblMaxScale),0);
//ISimplePointLayer member
m_bstrLPTFile.WriteToStream(pStm);
// legend group
IObjectStreamPtr ipObjStream(CLSID_ObjectStream);
ipObjStream->putref_Stream(pStm);
if (FAILED(hr = ipObjStream->SaveObject(m_ipLegendGroup)))
return hr;
return S_OK;
}
Note that the first thing you write is the persistence version of the classsee Chapter 2, 'Implementing Persistence',
for more information.
If the layer is properly persisted, the document in which it has been saved can be opened in the same state as when it
was last saved. You will need to return the layer information when the Load member of IPersistStream/IPersistVariant
is called.
[C++]

STDMETHODIMP CSimplePointLayer::Load(IStream * pStm)


{
short vers;
if (FAILED(pStm->Read(&vers, sizeof(vers), 0)))
return E_FAIL;
if (vers > cCurVers)
return E_FAIL;
HRESULT hr;
m_bstrName.ReadFromStream(pStm);
pStm->Read(&m_bVisible, sizeof(m_bVisible), 0);
pStm->Read(&m_bCached, sizeof(m_bCached), 0);
pStm->Read(&m_dblMinScale, sizeof(m_dblMinScale), 0);
pStm->Read(&m_dblMaxScale, sizeof(m_dblMaxScale), 0);
m_bstrLPTFile.ReadFromStream(pStm);
IObjectStreamPtr ipObjStream(CLSID_ObjectStream);
ipObjStream->putref_Stream(pStm);
hr = ipObjStream->LoadObject((GUID*) &IID_ILegendGroup, 0, (IUnknown**) &m_ipLegendGroup);
if (FAILED(hr))

return hr;

USES_CONVERSION;

158

m_fLPTFile.open(OLE2CA(m_bstrLPTFile));
if (!m_fLPTFile)
return E_FAIL;
return S_OK;
:
Implementing ILayerInfo
Once you have implemented IPersistStream/IPersistVariant, the layer can be saved as part of the map document. It
can also be saved outside the map document as a layer (.lyr) file. If you would like to associate a custom icon with the
layer file of your custom layer, you need to implement the ILayerInfo interface. If you do not implement this interface,
a plain layer icon will be associated with the layer file.

You will need to find a small and large icon to represent your layer. The small icon is typically a 16 by 16 pixel image;
the large icon is 32 by 32 pixels. Store the icons as resources in your project. Return the icons from the SmallImage
and LargeImage properties.
[C++]

STDMETHODIMP CSimplePointLayer::get_SmallImage(OLE_HANDLE* phBitmap)


{
if (!g_hSmallImage)
g_hSmallImage = (HBITMAP)::LoadImage(_Module.m_hInst, MAKEINTRESOURCE(IDB_SMALLBEXLYR),
IMAGE_BITMAP, 16, 16, LR_LOADTRANSPARENT);
*phBitmap = (OLE_HANDLE)g_hSmallImage;
return S_OK;
You can also add two other images to represent your layer when selected by returning the SmallSelectedImage and
LargeSelectedImage properties.
Implementing ILayerDrawingProperties
Implement the ILayerDrawingProperties interface so the map knows to redraw the layer when the layer's drawing
properties have changed. The DrawingPropsDirty member of this interface is automatically set when any of the layer's
attributes are changed in a property page. For example, if the minimum or maximum draw scales are set in the layer's
property page, DrawingPropsDirty will be set to True, and the map will be refreshed so that the layer can be redrawn
with the new drawing properties.
To implement DrawingPropsDirty, you simply need to store a boolean value.
[C++]

VARIANT_BOOL

m_bDrawDirty;

...
STDMETHODIMP CSimplePointLayer::put_DrawingPropsDirty(VARIANT_BOOL dirty)
{
m_bDrawDirty = dirty;
return S_OK;
Implementing IIdentify
To display attributes of features with the identify tool, the layer must implement IIdentify. This interface has a single
member, which should identify the feature at the specified location and return an array of objects that implement the
interface IIdentifyObj.

159

Do not carry out the Identify method if the layer is invalid.


[C++]

STDMETHODIMP CSimplePointLayer::Identify(IGeometry* pGeom, IArray** ppArrObj)


{
if (!m_bValid)
return S_OK;
..
The Identify method is passed in an IGeometry reference, which indicates the location at which to find the feature to
be identified. This geometry is an envelope object, which is constructed based on the search tolerance set in the map
(IMxDocument::SearchTolerancePixels) and the point specified by the identify tool. You will need to evaluate this
geometry (envelope) to see if it actually falls within the layer's extent. If it does not fall within the layer's extent, be
sure to return S_FALSE and an empty array of objects.
[C++]

// Check if input geometry envelope overlaps with spatial extent of layer


m_ipExtent->QueryEnvelope(ipLyrExt);//copy geometry
Note that the specified location will use the map's coordinate system. You will need to convert between the map's
spatial reference system and the datasource's native spatial reference system when comparing the IGeometry
reference with the layer's feature geometry.
[C++]

if (m_ipLayerSpatialRef)
ipLyrExt->Project(m_ipLayerSpatialRef);
pGeom->get_GeometryType(&shapeType);
if (shapeType != esriGeometryEnvelope)
pGeom->get_Envelope(&ipinEnv);
else
ipinEnv = pGeom;
ipinEnv->QueryEnvelope(ipIntersectEnv);
ipIntersectEnv->Intersect(ipLyrExt);
ipIntersectEnv->get_IsEmpty(&bEmpty);
// if the input geometry is not within the layer's extent:
// -pass back an empty array (i.e. count = 0
// -return S_FALSE
if (bEmpty == VARIANT_TRUE)
{
*ppArrObj = ipArray.Detach();
return S_FALSE;
}
...
To identify the feature, check each line in the data text file and find the point that falls within the specified location
(envelope). If a point is found and can be identified by the SimplePointIdObj object, the object is added to the array.
Note that because the dataset is small, looping through all the records to find the matching feature can be done
quickly. For larger files, an algorithm for spatial searches should be written. More details on the implementation of
IIdentifyObj follow in the next section, 'Creating the SimplePointIdObj'.
[C++]

STDMETHODIMP CSimplePointLayer::Identify(IGeometry* pGeom, IArray** ppArrObj)


{
...
while (hr != E_FAIL)
{
hr = NextRecord(&ipPt, &bstrAttr);
if (hr != E_FAIL)
{
// point is currently in the data's spatial reference system

160

ipPt->Project(m_ipLayerSpatialRef);
ipRelOp = ipPt;
ipRelOp->Within(pGeom, &bWithin);
// if point record matches the input geometry, add it to the array of IdentifyObjs
if (bWithin == VARIANT_TRUE)
{
ipIdObj.CreateInstance(CLSID_SimplePointIdObj);
ipIdObj->CanIdentify(this, &bIdentify);
if (bIdentify == VARIANT_TRUE)
{
ipLyrIdObj = ipIdObj;
ipLyrIdObj->put_Point(ipPt);
ipLyrIdObj->put_Character(bstrAttr);
ipArray->Add(ipIdObj);
}
}
}
}
*ppArrObj = ipArray.Detach();
return S_OK;
Creating and Implementing ISimplePointLayer
At this point, your layer lacks one essential piece of functionality. For a client object to be able to specify the layer's
datasource, define a new interface called ISimplePointLayer. Add one read-write property called FileName, and a
method called NextRecord, and implement the interface in the SimplePointLayer class.
When the FileName property is set, this will inform the layer where its data can be found and allow the layer to read
the data. You will use a stream type from the C++ standard library to read the data from the file.
[C++]

ifstream
1.

m_fLPTFile

First, before you attempt to open the file, ensure you close any file already open.
[C++]

STDMETHODIMP CSimplePointLayer::put_File(BSTR file)


{
m_bValid = VARIANT_FALSE;
m_bDrawDirty = VARIANT_TRUE;
m_ipExtent = 0;
m_bstrLPTFile = CComBSTR(file);
if (m_fLPTFile)
{
if (m_fLPTFile.is_open())
{
m_fLPTFile.close();
m_fLPTFile.clear();
}
m_sCurrentRow[0] = '\0';
}
2.

Then check that the new file exists and open the file.
[C++]

//check if input file exists:


if (GetFileAttributes(file)==-1)
{
this->put_Name(CComBSTR(_T("file not found")));
return S_OK;
}
USES_CONVERSION;
m_fLPTFile.open(OLE2CA(m_bstrLPTFile),ifstream::in);
if (!m_fLPTFile.good())
return S_OK;
3.

Find the basename of the file, and use this to set the ILayer::Name property.
[C++]

m_bValid = VARIANT_TRUE;

161

wchar_t* pwchar;
wchar_t* pwchar2;
pwchar = wcsrchr(m_bstrLPTFile.Copy(),'\\');
pwchar2 = wcstok(pwchar+1, _T("."));
CComBSTR bstrName(pwchar2);
m_bstrName.operator =(bstrName);
this->put_Name(bstrName);
return S_OK;
To implement the NextRecord method, check that the file is not at its end, and get the next line in the file. Parse the
line and create a Point with the coordinates you retrieved. Don't forget to set a reference to the SpatialReference of
the layer for each point you create.
[C++]

STDMETHODIMP CSimplePointLayer::NextRecord(IPoint** ppoint, BSTR *attribute)


{
// Read current row
if (!m_fLPTFile.eof())
{
char sAtt[2];
double x, y;
char* end;
char buf[6];
m_fLPTFile.getline(m_sCurrentRow, c_iMaxRowLen);
// First, parse the attribute out of the current row.
// We know this data source has just one attribute, which is one char wide.
strncpy(sAtt, m_sCurrentRow + 12, 1);
sAtt[1] = '\0'; // add null terminator
CComBSTR bstrAtt = CComBSTR(sAtt);
bstrAtt.CopyTo(attribute);
// Parse the X and Y values out of the current row and into the geometry
x = strtod(strncpy(buf, m_sCurrentRow, 6),&end);
y = strtod(strncpy(buf, m_sCurrentRow + 6, 6),&end);
IPointPtr ipPt(CLSID_Point);
ipPt->putref_SpatialReference(m_ipDataSpatialRef);
ipPt->put_X(x);
ipPt->put_Y(y);
*ppoint = ipPt.Detach();
}
else //eof, return E_FAIL
{
m_sCurrentRow[0] = '\0';
//set file pointer back to beginning
m_fLPTFile.clear();
m_fLPTFile.seekg(ios::beg);
return E_FAIL;
}
return S_OK;
}
In the VB sample project, a FileSystemObject is used to check for the presence of the data file, and get the file
extension and base name. A TextStream object is used to connect to the data file and read each line in turn. Both of
these objects can be found in the Microsoft Scripting Runtime object library.
As the file is not held on to permanently, and only to be read, the data can be updated while the layer is being viewed
in ArcMap, and when the data file is saved and the map refreshed, the Draw method will pick up any changes to the
file.
As an alternative implementation, you may want to cache the items in the file and check the file to see if it has been
edited before redrawing, then rereading and caching the file if it has been edited since the last read. Established data
sources include many measures to increase the speed of data refresh, for example, caching, spatial and attribute
indexes, or grouping of related records. The discussion of such issues is beyond the scope of this topic.
Now that the SimplePointLayer is complete, you need to provide the Identify class, which is required by the IIdentify
interface on your SimplePointLayer.

162

Creating the SimplePointIdObj

To identify the features of a layer using the identify tool, a few things are required. First, the layer must implement the
IIdentify interface. Second, the layer must have an associated IdentifyObj object to provide the identify results. At a
minimum, this IdentifyObj object must implement the IIdentifyObj interface and it must also provide a window in
which to display the identify results.
You will need to create a coclass called SimplePointIdObj that implements the IIdentifyObj interface. This is the only
ArcGIS interface the SimplePointIdObj will implement.
Implementing IIdentifyObj
Recall from the 'Implementing IIdentify' section above that for every feature that the IIdentify::Identify method
matches to the input geometry, the IdentifyObj associated with the layer is created. The IdentifyObj object verifies if it
can identify the features of the layer in its IIdentifyObj::CanIdentify method.
CanIdentify is passed an ILayer reference to the layer that the specified feature or features belong to. You will need to
QI the layer for the interface that uniquely identifies the layer that SimplePointIdObj is associated within this case,
ISimplePointLayer. Cache the layer reference so that you can supply the reference when your SimplePointIdObj is
asked for its Layer property.
[C++]

STDMETHODIMP CSimplePointIdObj::CanIdentify(ILayer* pLayer,VARIANT_BOOL* b)


{
*b = VARIANT_FALSE;
m_ipSimplePtLyr = pLayer;

//QueryInterface

if(m_ipSimplePtLyr==0)
return S_FALSE;
*b = VARIANT_TRUE;
return S_OK;
Once it is determined that a feature can be identified, the Layer object populates its associated IdentifyObj object with
the desired attributes of the specified feature (see the 'Implementing IIdentify' section above). The IdentifyObj object
can then populate its Identify Results window with these attribute values. As the standard Identify Results window
implementation will not work for the sample data, you will need to create a form or dialog box to display these
attribute values. This window will replace the right side of the Identify Results dialog box.
In the sample data, a single character attribute is present for each feature. Add an edit box to the dialog box to
display this attribute value. (In the VB sample project, a Label control is used.)
Now that the Identify Results window for the SimplePointIdObj has been completed, you can create this window and
provide its window handle in the implementation of the IIdentifyObj::hWnd property. This is also where you should
populate any of the window's control boxes with attribute values.

[C++]

STDMETHODIMP CSimplePointIdObj::get_hWnd(OLE_HANDLE* hWnd)


{
//create window
if (!m_IdentifyDlg)
m_IdentifyDlg =new CIdentifyDialog;
if (!m_IdentifyDlg->m_hWnd)
{
m_IdentifyDlg->Create(0);
m_IdentifyDlg->SetDlgItemText(ID_ATTRIBUTE, m_bstrAttr);
}
*hWnd =

(OLE_HANDLE)m_IdentifyDlg->m_hWnd;

163

return S_OK;
The name of the identified feature is specified in the string property IIdentifyObj::Name. This value is displayed in the
left window of the Identify Results dialog box.

The method IIdentifyObj::Flash is where you will put the code to flash the identified feature. Be sure to first verify that
there is an object to flash.
[C++]

STDMETHODIMP CSimplePointIdObj::Flash(IScreenDisplay* pDisplay)


{
if ((pDisplay ==0) || (m_ipPoint==0))
return S_FALSE;
...
Then you can highlight the feature by drawing and redrawing the Point.
[C++]

...
ISymbolPtr ipSym(CLSID_SimpleMarkerSymbol);
ipSym->put_ROP2(esriROPNotXOrPen);//erase itself when drawn twice
pDisplay->SetSymbol(ipSym);
//flash
OLE_HANDLE hDC;
pDisplay->get_hDC(&hDC);
pDisplay->StartDrawing(hDC, esriNoScreenCache);
pDisplay->DrawPoint(m_ipPoint);
::Sleep(300);
pDisplay->DrawPoint(m_ipPoint);//draw 2nd time to erase
pDisplay->FinishDrawing();
return S_OK;
Creating and implementing ISimplePointIdObj
To allow a client to set the point to be flashed and the character attribute to display in the Identify Results window,
define a new interface called ISimplePointIdObj. Add a write-only property of type esriGeometry.IPoint and write-only
property Char, which takes a Bstr. Implement this interface in the SimplePointIdObj class. Recall that earlier you used
the Point and Character properties of this interface in the IIdentify::Identify method of the SimplePointLayer layer.
[C++]

STDMETHODIMP CSimplePointIdObj::put_Point(IPoint *pPoint)


{
m_ipPoint = pPoint;
return S_OK;
}
STDMETHODIMP CSimplePointIdObj::put_Character(BSTR Attr)
{
m_bstrAttr.operator =(Attr);
return S_OK;

Layer Property Pages


Unless a property page is available for your layer, users are limited to editing the properties of the layer
programmatically. SimplePointLayerPropPage is a layer property page that will allow the user to change the data
source for the layer (for example, the path to the text file). Visual C++ developers will need to implement
IPropertyPage and IPropertyPageContext for their custom property page. Visual Basic developers should implement
IComPropertyPage. The implementation of these interfaces will be responsible for loading the property page and
determining if the property page applies to the specified layer.

164

Note that the Properties dialog box for a layer will also include other property pages, according to which interfaces are
implemented by the layer. For example, the General property page will apply to any layer, as it only requires the
ILayer interface to be implemented.

Creating the SimplePointPropPage

Create a new class called SimplePointLayerPropPage, by using a standard implementation of a property page. See
Chapter 2, 'Creating Property Pages', for further information on creating property pagesthis section will discuss only
the implementation details that apply specifically to the SimplePointLayer.
You will need to create a dialog box that will display the control or controls to edit the custom properties for your
layer. On the custom ISimplePointLayer interface, you added a single editable property for changing the file path to its
data source, so add a single EditBox to the dialog box. (The VB sample project uses a TextBox on a Form). You should
initialize the value in the edit box control to the existing value of ISimplePointLayer::File.

[C++]

LRESULT CSimplePointPropPage::OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)


{
CComBSTR bstrFilePath;
m_ipSimpPtLayer->get_File(&bstrFilePath);
SetDlgItemText(IDEB_FILEPATH, bstrFilePath);
return 0;
You will also need to handle any changes that occur on the property page and write them to the layer object. Capture
the OnChange event, which is fired when the text is altered, then flag the property page so it knows that its values
have changed.
[C++]

LRESULT CSimplePointPropPage::OnChangeFilepath(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL&


bHandled)
{
if (wNotifyCode == EN_CHANGE)
{
HRESULT hr = m_pPageSite->OnStatusChange(PROPPAGESTATUS_DIRTY);
if (FAILED(hr)) return hr;
m_bDirty = TRUE;
}
return 0;
Once the property page has been notified of the changes, the changes can be applied in its
IPropertyPageContext::QueryObject method.
[C++]

STDMETHODIMP CSimplePointPropPage::QueryObject(VARIANT theObject)


{
// Check if we have an ISimplePointLayerPtr
// If we do, apply the setting from the page.
CComVariant vObject(theObject);
if (vObject.vt != VT_UNKNOWN) return E_UNEXPECTED;
// Try and QI to ISimplePointLayerPtr
ISimplePointLayerPtr ipLyr(vObject.punkVal);
if (ipLyr != 0)
{
// Read the file Name from the property page and
// set the new value
BSTR bstrFilePath = ::SysAllocStringLen(0, 200);

165

GetDlgItemText(IDEB_FILEPATH, bstrFilePath);
HRESULT hr = m_ipSimpPtLayer->put_File(bstrFilePath);
if (FAILED(hr)) return hr;
::SysFreeString(bstrFilePath);
}
return S_OK;
}
If you are working in VB, you can retrieve changes to the property page values and flag the COM property page site
that the changes have occurred.
The property page must then be registered in the Layer Property Pages component category.
At this point, you can programmatically add the layer into ArcMap and change the Symbol. By using the Layer
Properties dialog box, you can also change the source of the layer's data, the minimum and maximum display scales,
and the layer's name.

You can also identify features.

Custom GxObjects for a custom Layer


Just as different types of Layers are used to represent the different types of datasets, different types of GxObjects are
used to represent the different data types. If you would like to support a custom data format in ArcCatalog, you will
need to start with encapsulating the data in a GxObject.
Once the GxObject is integrated with the framework and associated with a Layer, you will be able to browse to data in
ArcCatalog and add the data to a Map using the GxDialog object. If a data source is not tied to a GxObject but does
have a layer to represent it, the only way to add it to a map is programmatically (for example, via a VBA macro).
There are many different types of GxObject classes to represent the different types of data. All the items in the tree

166

and list views of ArcCatalog are some type of GxObject. For more information on GxObjects, please the Catalog library
in the Library section of the ArcGIS Developer Help system, and also see the examples and discussion of custom
GxObjects in Chapter 6 of this book.

Creating the SimplePointLayerGxObject

Add a new class to your project called SimplePointLayerGxObject, to represent the simple point data file so that it is
shown as an item in ArcCatalog. At a minimum, a GxObject should implement the ArcObjects interfaces IGxObject and
IGxObjectUI. These interfaces are used mostly to provide identification for the data format and an easily identifiable
icon. As with the custom layer, more interfaces can be implemented depending on the level of integration with ArcGIS
that is desired (again, see Chapter 6 for more information); for the SimplePointLayerGxObject, you will also implement
the IGxLayerSource interface.
As a great detail of information on creating a custom GxObject and GxObjectFactory is covered in Chapter 6, this
section will only cover the details of creating and integrating a GxObject so that you can browse for your custom
datasource in ArcCatalog and preview its associated layer or add it to a Map.
In addition to creating a custom GxObject, you will need to create a GxObjectFactory that knows how to manufacture
the GxObject. In the following section, therefore, you will create the SimplePointLayerGxObjectFactory.
Creating and Implementing ISimplePointLayerGxObject
Define an interface called ISimplePointLayerGxObject. The interface should contain one read-write property called
FileName, to allow the location of the data to be set by the SimplePointLayerGxObjectFactory (see below). When the
SimplePointLayerGxObjectFactory object creates a SimplePointLayerGxObject, it passes the full path of the datasource
to its ISimplePointLayerGxObject::FileName property.
Implementing IGxObject
Once a GxObject knows its name via the ISimplePointLayerGxObject interface you just created, it can populate the
name properties IGxObject::BaseName, IGxObject::FullName, and IGxObject::Name.
[C++]

STDMETHODIMP CSimplePointLayerGxObject::get_Name(BSTR * Name)


{
if (Name == NULL)
return E_POINTER;
// file name with no extension
wchar_t* pwchar;
pwchar = wcsrchr(m_bstrFileName.Copy(),'\\');
CComBSTR bstrName(pwchar+1);
bstrName.CopyTo(Name);
return S_OK;
}
The easiest way to assemble the FullName for the GxObject is by calling IGxCatalog::ConstructFullName on the
GxCatalog object and passing in a reference to itself. (The GxCatalog reference is cached when the IGxObject::Attach
member is called.)
[C++]

STDMETHODIMP CSimplePointLayerGxObject::get_FullName(BSTR * Name)


{
// file name and path
if (Name == NULL)
return E_POINTER;
m_ipCatalog->ConstructFullName(this, Name);
return S_OK;
}
For the InternalObjectName property, you can return a custom Name object, SimpleLayerPointName. (More details on
this object follow in a later section.) The InternalObjectName property is the link to the underlying data which is
encapsulated by the GxObject. It is this Name object that references the underlying data and not the GxObject itself.
[C++]

STDMETHODIMP CSimplePointLayerGxObject::get_InternalObjectName(IName * * InternalObjectName)


{
if (InternalObjectName == NULL)
return E_POINTER;

167

//InternalObjectName is passed to all layerfactories


ISimplePointLayerNamePtr ipLyrName(CLSID_SimplePointLayerName);
INamePtr ipName(ipLyrName);
if (ipName==0)
return E_FAIL;
ipName->put_NameString(m_bstrFileName);
*InternalObjectName = ipName.Detach();
return S_OK;
}
Implementing IGxObjectUI
The members of the IGxObjectUI interface provide ArcCatalog with the icons that will represent the GxObject. As with
the implementation of the ILayerInfo interface, you will need to provide a small and large icon for the
SimplePointLayerGxObject. If this interface is not implemented, a plain icon will be associated with the GxObject.

If IGxObjectUI is not implemented, a plain icon will be associated with the GxObject instead.
Implementing IGxLayerSource
If the data associated with a GxObject is to be added to a Map via a GxDialog window, the GxObject must be a Gx
dataset object or a Gx layer source. To identify the SimplePointLayerGxObject object as a layer source, it must
implement the IGxLayerSource interface. IGxLayerSource is an indicator interface, which has no members.

Creating the SimplePointLayerGxObjectFactory

As you have read, each of the GxObject classes has a corresponding GxObjectFactory. Add a new class to your project,
called SimplePointLayerGxObjectFactory, to define a GxObjectFactory responsible for generating
SimplePointLayerGxObject objects.
At a minimum, you should implement IGxObjectFactory interface for the factory class; its members will determine if a
folder contains the relevant data, and create GxObjects to represent the data if it is the supported data format. While
only the IGxObjectFactory interface needs to be implemented to create a functioning GxObjectFactory, every
GxObjectFactory object should also implement the IGxObjectFactoryFileExtensions interface. This interface provides a
set of file extensions that are handled by the factory. Only those files that match the specified extensions will be
inspected by the GxObjectFactory.
When the SimplePointLayerGxObjectFactory determines that a folder contains its simple point datafiles, the
SimplePointLayerGxObjectFactory will instantiate a SimplePointLayerGxObject object to encapsulate the data. To
facilitate the inspection of the folders for the datafiles, SimplePointLayerGxObjectFactory also implements the interface
IGxObjectFactoryFileExtensions. Only those files with the correct file extension (.lpt) will be inspected by the
SimplePointLayerGxObjectFactory. This greatly speeds up the process of finding children for the GxObjectFactory. If
this interface was not implemented, SimplePointLayerGxObjectFactory would be passed to every filename in the folder
to verify if it is a child.
Implementing IGxObjectFactory
The IGxObjectFactory interface allows GxObjectFactory objects to return the factory's name and information about its
potential children. The Name property of IGxObjectFactory indicates which type of data is associated with the
GxObjectFactory. The name of the GxObjectFactory will appear in the list of data types registered with ArcCatalog
(Tools->Options->General tab).

168

The method HasChildren is passed an IFileNames reference to inspect for a given folder. If the interface
IGxObjectFactoryFileExtensions has been implemented by the factory, only the relevant files will be passed to the
HasChildren method by ArcCatalog. Since the SimpleGxObjectFactory object is only interested in a single file
extension, you can be sure that every file name passed to this method will have the correct file extension and points
to a datasource. However, since the HasChildren method may be called by a client other than ArcCatalog, it would still
be prudent to inspect each file for the relevant file extension. If the folder does contain any simple point data types,
indicate that the folder does have children.
[C++]

STDMETHODIMP CSimplePointLayerGxObjectFactory::HasChildren(BSTR parentDir, IFileNames* pFileNames,


VARIANT_BOOL* pHasChildren)
{
*pHasChildren = VARIANT_FALSE;
CComBSTR bstrFileName;
pFileNames->Next(&bstrFileName);
while (bstrFileName != 0)
{
bstrFileName.ToUpper();
wchar_t* pwchar;
wchar_t* pwchar2;
pwchar = wcsrchr(bstrFileName.Copy(),'\\');
wcstok(pwchar, _T("."));
pwchar2 = wcstok(NULL, _T("."));
CComBSTR bstrName(pwchar2);
if (bstrName.operator ==(_T("LPT")))
{
*pHasChildren = VARIANT_TRUE;
break;
}
pFileNames->Next(&bstrFileName);
}
return S_OK;
}
If more than one file type is associated with your dataset, you should inspect all the filenames in the array to ensure
that all the necessary files are contained in the given folder.

169

The GetChildren method is passed some of the same parameters as were passed to the HasChildren method.
GetChildren will only be called by ArcCatalog if the call to HasChildren indicated that the given folder contained the
supported data type. To implement GetChildren, iterate the FileNames received for the data type with the appropriate
file extension; however, this time a SimplePointLayerGxObject needs to be created and returned in a GxObject
enumeration. Additional inspection of the file is recommended to verify that it references a valid dataset.
[C++]

STDMETHODIMP CSimplePointLayerGxObjectFactory::GetChildren(BSTR parentDir, IFileNames* pFileNames,


IEnumGxObject** ppChildren)
{
IGxObjectArrayPtr ipGxObjArray(CLSID_GxObjectArray);
ISimplePointLayerGxObjectPtr
IGxObjectPtr

ipGxChild;

ipGxObj;

CComBSTR bstrFileName;
pFileNames->Next(&bstrFileName);
while (bstrFileName != 0)
{
wchar_t* pwchar;
wchar_t* pwchar2;
pwchar = wcsrchr(bstrFileName.Copy(),'\\');
wcstok(pwchar, _T("."));
pwchar2 = wcstok(NULL, _T("."));
CComBSTR bstrName(pwchar2);
bstrName.ToUpper();
if (bstrName.operator ==(_T("LPT")))
{
ipGxChild.CreateInstance(CLSID_SimplePointLayerGxObject);
ipGxChild->put_FileName(bstrFileName);
ipGxObj = ipGxChild;
ipGxObjArray->Insert(-1, ipGxObj);
pFileNames->Remove();
}
bstrFileName.Empty();
pFileNames->Next(&bstrFileName);
}

IEnumGxObjectPtr ipEnum(ipGxObjArray);
*ppChildren = ipEnum.Detach();

return S_OK;
}
Note that the filename is written to the GxObject so that the GxObject knows where the data is located.
Implementing IGxObjectFactoryFileExtensions
The members of the interface, IGxObjectFactoryFileExtensions, simply indicate which file extensions are associated
with the GxObjectFactory. The property ActivationExtensions indicates the minimal set of file extensions that should
cause the factory to be activated. The property RelevantExtensions indicates the complete set of file extensions
relevant to the factory. If you have a data type that has multiple files associated with it, then you will need to specify
every file extension that is required to successfully load your data, separated by a pipe (|) character. For example, a
layer file is a single file with the extension .lyr, but it may have an associated .xml file, so the GxLayerFactory returns
"lyr" for ActivationExtensions and "lyr|xml" for RelevantExtensions.
For the SimplePointLayer, only one file extension applies, which is '.lpt'. If the specified file extension does not match
any of the file extensions in a given folder, the factory will not be activated.
[C++]

STDMETHODIMP CSimplePointLayerGxObjectFactory::get_RelevantExtensions(BSTR* extSet)


{
CComBSTR bstr(_T("lpt"));

170

bstr.CopyTo(extSet);
return S_OK;
}
STDMETHODIMP CSimplePointLayerGxObjectFactory::get_ActivationExtensions(BSTR* extSet)
{
CComBSTR bstr(_T("lpt"));
bstr.CopyTo(extSet);
return S_OK;
}
Creating and Implementing ISimplePointLayerGxObjectFactory
To be able to uniquely identify your GxObject factory class, you should define and implement a new interface called
ISimplePointLayerGxObjectFactorythis interface does not require any members, as its only function is identification.
Now register the SimplePointLayerGxObjectFactory to the ESRI GX Object Factories component category so that
ArcCatalog can find the factory and account for the data source.

LayerFactories, Enumerations, and Names


Every Layer object should have a LayerFactory that is responsible for generating the layer. CadLayer objects, for
example, can be created by the CadLayerFactory. All layer factories must implement the interface ILayerFactory and
must be registered in the Layer Factory component category.
Layer factories are used by ArcCatalog to generate map layers for a given GxObject. If a GxObject encapsulates
geographic data that can be viewed as a layer, the layer's factory will assume the task of generating the Layer object
for the data. Once the layer has been created for a GxObject, it can be viewed in a Map.
For example, before a GxObject can be previewed in the geography view window in ArcCatalog, its associated
LayerFactory object is asked to create the layer that will be viewed in the Map. More precisely, what happens is the
IGxObject::InternalObjectName property is retrieved from the GxObject to obtain a Name object that represents the
data. Name objects are used extensively by ArcCatalog to browse datasets and indicate their location. Remember that
it is the Name object that references the data and not the GxObject itself. All registered layer factories are then
prompted if they can create (ILayerFactory::CanCreate) the data. If a layer factory does apply to the given object, the
method ILayerFactory::Create is finally called to generate the layer or layers to add to the map. This method returns
the layer or layers in an enumeration.

171

The illustration above shows the process for previewing the Layer for a GxObject in ArcCatalog.

Creating the SimplePointLayerFactory

Add a new class to your project called SimplePointLayerFactory, which will be responsible for generating the
SimplePointLayer. Implement the ILayerFactory interface. This is the only interface you need to implement on the
custom layer factory.
The SimplePointLayerName object is returned when the IGxObject::InternalObjectName property of the
SimplePointLayerGxObject is retrieved. The layer factory, SimplePointLayerFactory, creates an instance of the custom
layer enumeration, SimplePointLayerEnumLayer. More details on these objects will follow in the sections below.
Implementing ILayerFactory
The CanCreate method of ILayerFactory is passed in an IUnknown reference to a Name object, which is the internal
object that the given GxObject represents. You will need to query this Name object to determine if it represents the
desired dataset. For the simple point data source, the Name object will be SimplePointLayerName). Therefore, to
determine if your layer factory can create the layer for the input object, QI the object for the ISimplePointName
interface. If the QI succeeds, cache the IName::NameString value and indicate that the layer can be created.
[C++]

STDMETHODIMP CSimplePointLayerFactory::get_CanCreate(IUnknown * inputObject, VARIANT_BOOL * ok)


{
if (!ok)
return E_POINTER;
*ok = VARIANT_FALSE;
//see if input is an SimplePointLayerName, get File path
ISimplePointLayerNamePtr ipLyrName(inputObject);
if (ipLyrName)
{
INamePtr ipName(ipLyrName);
ipName->get_NameString(&m_bstrFileName);
}
else
return S_OK;
*ok = VARIANT_TRUE;
return S_OK;
}
If you returned true from CanCreate, then the Create method will be called by ArcCatalog to generate the Layer
object. This method is passed the same IUnknown reference to the Name object that CanCreate was passed, but this
time you must create the SimplePointLayer object for the dataset. Use the ISimplePointLayer interface, which you

172

created earlier to set the location of the data represented by the layer. You should then return a reference to the layer
object in a layer enumeration.
[C++]

STDMETHODIMP CSimplePointLayerFactory::Create(IUnknown * inputObject, IEnumLayer * * Layers)


{
ISimplePointLayerNamePtr ipLyrName(inputObject);
if (ipLyrName)
{
INamePtr ipName;
ipName = ipLyrName.Detach();
ipName->get_NameString(&m_bstrFileName);
}
else
return E_FAIL;
ISimplePointLayerPtr ipSPLyr(CLSID_SimplePointLayer);
ipSPLyr->put_File(m_bstrFileName);
ILayerPtr ipLyr(ipSPLyr);
ISimplePointEnumLayerPtr ipSimplePtEnumLyr;
CComObject<CSIMPLEPOINTENUMLAYER>* pobj = NULL;
CComObject<CSIMPLEPOINTENUMLAYER>::CreateInstance(&pobj);
pobj->Init(ipLyr);
pobj->QueryInterface(&ipSimplePtEnumLyr);
if (ipSimplePtEnumLyr ==0)
return E_FAIL;
IEnumLayerPtr ipEnum(ipSimplePtEnumLyr);
*Layers = ipEnum.Detach();
return S_OK;
}
As shown in the code above, it is still prudent to evaluate the Name object in case the Create method is called by a
different client that does not call the CanCreate method first.
The reason that a layer enumeration is used is to account for data sources, which may contain multiple datasets for a
single Namefor example, a CAD file.
Creating and Implementing ISimplePointLayerFactory
To be able to uniquely identify your layer factory class, you should define a new interface called
ISimplePointLayerFactory and implement this in the layer factory class. This interface does not require any members,
as its only function is identification.

Creating the SimplePointLayerName

Now create the custom Name object you will need to identify the simple point datasource. Add a new class to your
project called SimplePointLayerName. Although there is no Name abstract class to base your Name object on, you can
see by the other Name objects that such a class should implement at least IName and IPersistStream/Variant.
Recall that a SimplePointLayerName object is returned when a SimplePointLayerGxObject is prompted for its
IGxObject::InternalObjectName. It is this Name object that references the datasource for the GxObject.
Implementing IName
The SimplePointLayerName object will need to identify and locate the simple point data file. The NameString property
will be populated when the IGxObject::InternalNameObject property is retrieved.
[C++]

STDMETHODIMP CSimplePointLayerName::put_NameString(BSTR NameString)


{
m_bstrName = NameString;
return S_OK;
}

173

STDMETHODIMP CSimplePointLayerName::get_NameString(BSTR * NameString)


{
if (NameString == NULL)
return E_POINTER;
*NameString = m_bstrName.Copy();
return S_OK;
}
Name Objects can also be used as lightweight references to the objects they represent. The Open method can be
called to actually instantiate a dataset. Since the simple point datasource has been integrated as a Layer object, the
Open method should return the SimplePointLayer object.
[C++]

STDMETHODIMP CSimplePointLayerName::Open(IUnknown * * unknown)


{
if (unknown == NULL)
return E_POINTER;
if (m_bstrName.operator !())
return S_FALSE;
// instantiate the object this Name represents
ISimplePointLayerPtr ipSPLyr(CLSID_SimplePointLayer);
ipSPLyr->put_File(m_bstrName);
*unknown = ipSPLyr.Detach();
return S_OK;
}
Creating and Implementing ISimplePointLayerName
To be able to uniquely identify your name class, you should define a new interface called ISimplePointLayerName and
implement this in the name class. This interface does not require any members, as its only function is identification.
Once the IName::Open method has been implemented, the Open method can be called by client code to add a simple
point layer to a Map. This is demonstrated by the following ArcMap macro code:
[Visual Basic]

Dim pMxDoc As IMxDocument


Set pMxDoc = ThisDocument
Dim pMap As IMap
Set pMap = pMxDoc.FocusMap
Dim pGxDialog As IGxDialog
Set pGxDialog = New GxDialog
Dim bool As Boolean
Dim pEnumGxObj As IEnumGxObject
bool = pGxDialog.DoModalOpen(0, pEnumGxObj)
If bool Then
Dim pGxObject As IGxObject
Set pGxObject = pEnumGxObj.Next
Dim pName As IName
Set pName = pGxObject.InternalObjectName
Dim pUnk As IUnknown
Set pUnk = pName.Open
If TypeOf pUnk Is ILayer Then
Dim pLyr As ILayer
Set pLyr = pUnk
pMap.AddLayer pLyr
End If
End If

174

Creating the SimplePointEnumLayer


When a layer factory creates a Layer object, it is put into a layer enumeration, and a reference to the IEnumLayer
interface of the enumeration object is returned (see the 'Implementing ILayerFactory' section above. Since there are
no ArcGIS classes that implement IEnumLayer, you will need to provide your own implementation.

Add a new class to your project called SimplePointEnumLayer, to work with the SimplePointLayerFactory object, and in
it implement the IEnumLayer interface.
The IEnumLayer interface has two methods: Next and Reset. The Next method returns an ILayer reference to the next
SimplePointLayer object in the set and advances the internal pointer. There will be only one layer in the collection so
the end of the set will be reached after the first call. When the end of the enumeration is reached, be sure to return
S_FALSE. For the SimplePointEnumLayer class, you can store the collection of layers in a Standard Template Library
(STL) vector class.
Implementing IEnumLayer
The IEnumLayer interface provides access to members that allow iteration through a set of Layer objects. You will
need to pass the collection of layers to the enumerator class so it can traverse the list of items. Please see the project
source code for one possible solution for linking the enumerator class with the collection of layersas the class uses
STL, these details will not be discussed in this section.
[C++]

STDMETHODIMP CSimplePointEnumLayer::Next(ILayer * * Layer)


{
if (Layer == NULL)
return E_POINTER;
if (m_LayerVecIdx > 0)
{
*Layer = NULL;
return S_FALSE;
}
ILayerPtr ipLyr;
ipLyr = m_vectLayer.front();
*Layer = ipLyr.Detach();
m_LayerVecIdx++;
return S_OK;
}
In the Reset method, reset the internal pointer to the beginning of the set.
[C++]

STDMETHODIMP CSimplePointEnumLayer::Reset()
{
m_LayerVecIdx =0;
return S_OK;
}
Creating and Implementing ISimplePointEnumLayer
To be able to uniquely identify your layer enumeration class, define a new interface called ISimplePointEnumLayer,
and implement this in your SimplePointEnumLayer class. This interface does not require any members, as its only
function is identification. It will be used in the SimplePointLayerFactory's Create member.
Now that you have a LayerFactory, Name, and enumeration for your SimplePointLayer, you will be able to see the
datasource and show the geographical preview in ArcCatalog.
See Also Creating Cartography, About Custom Layers.

175

Chapter 5: Extending the Display


The following sections provide examples of creating custom symbols and renderers. Symbols generally reside in the
Display library; Renderers generally reside in the Carto library.
Symbols
Creating Custom Symbols
Introduction to the Symbols object model and creating custom symbols
Logo Marker Symbol Example
An example of a custom marker symbol allowing you to draw the shape of a specific logo on the screen based on a
Point geometry
Vertex Line Symbol Example
An example of a custom line symbol allowing you to draw a line geometry highlighting its vertices
Creating other kinds of custom symbols
Advice on creating other kinds of custom symbols
Renderers
Custom feature renderers
Introduction to the Symbols object model and creating custom symbols
Point Dispersal Renderer Example
An example of a custom renderer allowing you to disperse coincident points to help you view your data
Managing custom feature renderers
Advice on how you can apply custom renderers to layers

Customizing the Display


The following sections provide examples of creating custom symbols and renderers. Symbols generally reside in the
Display library; Renderers generally reside in the Carto library.
Symbols
Creating Custom Symbols
Introduction to the Symbols object model and creating custom symbols
Logo Marker Symbol Example
An example of a custom marker symbol allowing you to draw the shape of a specific logo on the screen based on a
Point geometry
Vertex Line Symbol Example
An example of a custom line symbol allowing you to draw a line geometry highlighting its vertices
Creating other kinds of custom symbols
Advice on creating other kinds of custom symbols
Renderers
Custom feature renderers
Introduction to the Symbols object model and creating custom symbols
Point Dispersal Renderer Example
An example of a custom renderer allowing you to disperse coincident points to help you view your data
Managing custom feature renderers
Advice on how you can apply custom renderers to layers

Creating custom symbols


A symbol is a class that can draw things, such as points, lines, and areas, to a display. The Display object model
contains a range of MarkerSymbols, LineSymbols, and FillSymbols, which can be used in conjunction with graphic
elements or renderers to draw features, graphics, map surrounds, and so on. These can be combined into multilayer
symbols to achieve a more complex display.
There are a number of different symbols available in ArcObjects, which are used to draw points,
lines, areas, and text.
The TextSymbol can be used to draw text to the screen and is commonly used in labelling, annotation, and graphic
elements. The more specialized 3DChartSymbols are used in conjunction with the ChartRenderer.

176

Is a custom symbol the right solution?


If none of the standard symbols can draw your features or graphics the way you require, you may find it useful to
implement your own custom symbol. Custom symbols can be applied to any graphic element or feature, they can take
part in other multilayer symbols and renderers, they can be edited by users like other symbols, and they can be saved
and retrieved as StyleGalleryItems.
Multiple symbols of a similar type can be combined into a multilayer symbol.
If your drawing requirements are not met by these symbols, try implementing a custom symbol.
Before you go ahead and create a new type of symbol, you might like to review your alternatives to check that a
custom symbol is your most appropriate action.
The clever combination and manipulation of the existing symbols can result in a great range of display options.
CharacterMarkerSymbols, CartographicLineSymbols, and PictureFillSymbols in particular are flexible, and when you
combine effects in a multilayer marker, line, or fill symbol, a wide range of effects can be achieved.
A good knowledge of the available options in ArcMap will help you decide, but you should also review the Display
object modelit may be able to manipulate the existing symbols programmatically in a way that you cannot achieve
using the ArcMap user interface.
Before implementing a custom symbol, check that you cannot get the effect you require through
manipulation of the existing symbols.
A custom symbol is a relatively low-level solution; for example, it may exist without the presence of an MxDocument.
It should never rely on the attributes of a particular featureif required, you should consider a custom renderer
instead. Also, a symbol does not generally change the location of an itemprojections or the transformation of your
data may be more appropriate here.
As you have programmatic access to the ScreenDisplay, you will find it is possible to draw items directly to the display
without using a symbol, feature, or element. This type of solution may be appropriate to temporarily highlight the
result of an operation, for example in the way that a feature is 'flashed' on the display when you select that feature in
the Identify dialog box.
In general, drawing directly to the display may not be suitable. If your drawings need to be persisted with the
document or after refreshing the view or if user interaction with the shape is required such as selection and editing,
direct drawing may not be suitable.
Using custom symbols
Once you have decided on a custom symbol, you need to consider your implementation detailshow you can achieve
the drawing effects you require.
When planning and testing your symbol, consider issues such as drawing efficiency and platform function support.
Make sure you are familiar with the API you are using and surrounding issues. Consider your drawing efficiencyusers
may find it particularly frustrating waiting for drawing to complete on complex maps. Also consider platform support
for GDI functionsyour symbol may be drawn to a screen, exported to a file, or output to any type of printer.
Symbols may be drawn to many different types of devicesoutput files, printers, and screens.
Ensure your drawing methods are suitable for these devices.
The Windows GDI is a mature platform for developers, and you should be able to find much information in the
references in the bibliography and MSDN for further reading on this extensive topic.
Similarly, for those choosing to use alternative methods of drawing, efficiency and platform support should be
considered, in addition to any issues specific to the method you are using.
Examples in this chapter
In the following sections you will see a custom MarkerSymbol, the simplest type of symbol. Many issues of designing
and implementing a custom symbol are common to implementing a marker, line, fill, text, or chart symbol and are
discussed in this example. In addition, there follows an example of a line symbol, which demonstrates not only a
different type of symbol, but a different approach to drawing the symbol itself.

177

This chapter demonstrates how to create a custom symbol to draw points and lines.
See Also Logo Marker Symbol Example, Vertex Line Symbol Example, and Creating other types of custom symbol.

Logo Marker Symbol Example


Object Model Diagram

Example Code Click here


Description This example provides a custom symbol, which draws a company logo to symbolize a point. Simple
custom functionality is provided to alter the colors of the different parts of the symbol, and a property page is also
provided to allow end users to edit the properties of the symbol.
Design Coclass LogoMarkerSymbol is a subtype of the MarkerSymbol abstract class. LogoMarkerPropertyPage is an
accompanying property page coclass.
License required ArcView or above
Libraries Framework, Display, DisplayUI, Geometry, and System
Languages Visual Basic (some restrictions), Visual C++
Categories Marker Symbols, ESRI Property Pages, and Symbol Property Pages
Interfaces IClone, ISymbol, IPersist, IMarkerMask, IMapLevel, IMarkerSymbol, ISymbolRotation, IDisplayName,
IPropertySupport, IComPropertyPage, IPropertyPageContext, ISymbolPropertyPage
How to use
1.

If using VB, register LogoMarkerSymbolVB.dll and double-click the LogoMarkerSymbolVB.reg file to register to
component categories.
If using VC++, open and build the project LogoMarkerSymbolVC.dsp to register the DLL and register to
component categories.

2.

Open ArcMap and add a layer with point features or a marker graphic element. Open the Symbol Selector for the
item.
For a layer, right-click the layer in the ArcMap table of contents, click Properties, then in the Layer Properties
dialog box, click the Symbology tab. The Single Symbol renderer should be selected by default; click the Symbol
button to show the Symbol Selector.
For a marker element, right-click the element and click Properties. In the Properties dialog box, make sure the
Symbol tab is selected, and click the Change Symbol button.

3.

In the Symbol Selector dialog box, click the Properties button to display the Symbol Editor.

4.

In the Symbol Editor dialog box, pull down the Type list box and click Logo Marker Symbol.
You can now set the properties of a LogoMarkerSymbol. Click OK to select the symbol and return to the Symbol
Selector.

Case for a custom Marker symbol


Imagine that the fictitious company logo shown here must be used to symbolize point features or graphic elements.
You must be able to use it repeatedly, as part of a renderer or graphic, and at a wide variety of scales including large
format output. You must also add the ability to alter the color of each section of the logo to indicate different divisions

178

of the company.

To create a symbol like this by using the core ArcObjects symbol classes, you have a couple of options available.
You could create a PictureMarkerSymbol, as this may be used effectively to portray any design. However, changing the
colors of the logo sections would require a different bitmap for each possible color combination. Also,
PictureMarkerSymbols may appear pixelated when zoomed in; using a high resolution bitmap may solve this problem,
but can also increase memory requirements, and slow down draw speeds.

Alternatively, you could construct a MultiLayerMarkerSymbol, with separate CharacterMarkerSymbols to represent the
different parts of the logo. As the symbol is drawn with vectors, there would be no resolution problems. However, you
would need to create a specialist TrueType font with glyphs designed to represent the different sections of the logo.
As no core symbol coclass provides the functionality you require, you can create a custom marker symbol.
This example provides a custom symbol that draws a company logo. Different colors can be used for
the sections of the symbol.

Creating a subtype of MarkerSymbol

If you decide to create a custom symbol, start by reviewing the Display object diagram. You will see that all Symbol
classesmarkers, lines, fills, text, and chartsinherit from a common abstract class called Symbol.
Therefore, any type of custom symbol you create must begin by implementing the ISymbol interface, along with
interfaces for cloning and persistence.
Any class that implements ISymbol can be drawn to a device; however, classes specialize in the type of objects they
can draw.

Looking again at the Display object model diagram, you can see that each coclass for drawing point features also
inherits from the MarkerSymbol abstract class.
Therefore, to create a MarkerSymbol, you should also implement IMarkerSymbol, ISymbolRotation, IMapLevel, and
IPropertySupport.
Looking at the existing MarkerSymbol classes, you can see many of them also implement IMarkerMask. This interface
provides the ability to draw a standard mask around a MarkerSymbol, which can be useful when placing multicolored
symbols on a multicolored background, as it helps the eye to identify the boundaries of the symbol more clearly. This
interface is, therefore, also an appropriate interface to implement in this case.

179

A marker mask can help to distinguish symbols from a similarly colored background.
All MarkerSymbols also implement IDisplayName, which provides a string description of each type of symbol and which
is used in the Symbol Properties Editor dialog box.
IPropertySupport cannot be implemented in VB
Note that it is not possible to implement IPropertySupport in VB. This will not affect the main functionality, as ArcMap
does not assume that this interface is implemented, but may check for its presence on any MarkerSymbol.
Most of the discussion for this example centers on the VB example project, as the approach taken is the same
regardless of the development environment. The implementation of IPropertySupport is discussed for the benefit of
those developing in VC++.

Creating the LogoMarkerSymbol

To solve the requirements of this example, you will create a subtype of MarkerSymbol, called LogoMarkerSymbol,
registered to the Marker Symbols component category.
You will implement ISymbol, IMarkerSymbol, ISymbolRotation, IMapLevel, IMarkerMask, and IDisplayName, as well as
the standard interfaces for cloning and persistence. To add the custom functionality, you will also create and
implement a custom interface, ILogoMarkerSymbol.
Techniques for drawing
There are a number of ways you could perform the actual drawing of a symbol.
You can use the GeometryDraw class or the ISymbol::Draw or IDisplay::Draw methods. In this case, the shape of the
logo would be stored as existing geometries (Polygons, Polylines, Envelopes, and so forth). You will be limited to
drawing with existing geometries and symbols, but this approach does allow you to utilize the full functionality of
ArcObjects to transform and adapt the shape and appearance of your symbol as required. This design may suit the
production of a scale-dependent symbol, for example, that renders differently according to the current display scale. It
may also suit a VB programmer who does not want to apply the alternative techniques discussed below.
You may decide to perform drawing operations using third party drawing libraries, or the low-level libraries available
as part of the Windows platform. You may want to investigate the OpenGL standard or the Windows-specific DirectX
libraries. Note that both were originally designed for use by C++ programmers and may not be a straightforward
programming task in non-C++ environments.
In this example, you will use the Windows Graphics Device Interface (GDI) functions to draw the symbol.
Using GDI calls can produce efficient draw routines and also offers flexibility in the kind of drawing you can do.
However, you need to be familiar with using GDI calls; some VB programmers may not have used these before. Also,
you may need to perform extensive mathematical calculations to transform your symbol's coordinates according to
Size, Angle, and so on. As Windows GDI functions require instructions in device coordinates, you will store the shape
of the logo in device coordinates.
Implementing ISymbol
The ISymbol interface is responsible for actually drawing a geometry to the appropriate device context, using the
correct appearance, shape, size, and location.
When a refresh event is called, ArcMap will work out which shapes need to be drawn and in which
order. It then uses the ISymbol interface to request that the shape draw itself.
Before any ISymbol is drawn, its SetupDC method is called, which receives information about the drawing device.
Then the Draw method is called, which receives the shape and location (the Geometry) of the item to be drawn.
Finally, the ResetDC method is called.
A general overview of the actions that should be performed by a custom symbol during each of these members is
given below. This can be used as a guide for any symbol drawn using GDI functions.

180

If you make use of GDI calls to draw your symbol, you should use the SetupDC and ResetDC
members of ISymbol to handle the adding and release of GDI objects, device contexts, and handles.
The actions performed in each of the draw methods are summarized here.
You will use the CreatePen and CreateSolidBrush GDI functions to define the appearance of a LogoMarkerSymbol, and
the Chord and Polygon functions to draw the sections of the symbol to the device context. You will also need to use
the SelectObject and DeleteObject GDI functions to maintain the device context objects correctly.
Add these declarations to your project (in the VB project, they are located in the basUtility.bas module). Also, declare
a user-defined type called POINTAPI, as GDI functions require coordinates to be defined as POINTAPI structures.
[Visual Basic 6]

Public Type POINTAPI


x As Long
y As Long
End Type
Now define an array of POINTAPI structures as a member variable of the LogoMarkerSymbol class. This array will hold
the control points, which are the significant points you will use to define the shape and location of the logo in device
coordinates.
[Visual Basic 6]

Private m_pCoords(6) As POINTAPI


The control points used by the drawing methods are stored in the m_pCoords array. They define the
locations used for the Chord and Polygon GDI calls.

Now you can begin coding the ISymbol methods.


SetupDC method
In SetupDC you need to prepare the class members to draw to the specific device, which is passed in as parameters to
this method (hDC and displayTransformation).
1.

First, store the passed-in information.


[Visual Basic 6]

Set m_pTrans = Transformation


m_lhDC = hdc
2.

Next, set up the device ratio. See the Null transformations and resolution in the Draw and QueryBoundary
section later for more information.
[Visual Basic 6]

SetupDeviceRatio m_lhDC, m_pTrans


3.

Calculate the size of the symbol in device coordinates. You will use these later in Draw.
[Visual Basic 6]

m_dDeviceRadius = (m_dSize / 2) * m_dDeviceRatio


m_dDeviceXOffset = m_dXOffset * m_dDeviceRatio
m_dDeviceYOffset = m_dYOffset * m_dDeviceRatio
4.

Now you are ready to create the pens and brushes, which you will use to fill and outline the sections of the
symbol, and set up the ROP2 code used for the drawing. Save the existing values for all the GDI objects you will
change, so you can replace these in ResetDC.
[Visual Basic 6]

m_lPen = CreatePen(0, 1 * m_dDeviceRatio, m_pColorBorder.RGB) 'Scale Pen


m_lOldPen = SelectObject(hdc, m_lPen)
m_lROP2Old = SetROP2(hdc, CLng(m_lROP2))
m_lBrushTop = CreateSolidBrush(m_pColorTop.RGB)

' Draws Chord

m_lBrushLeft = CreateSolidBrush(m_pColorLeft.RGB) ' Draws left Poly


m_lBrushRight = CreateSolidBrush(m_pColorRight.RGB) ' Draws right Poly

181

Draw method
In the Draw method, work out the location of each control point for the symbol, and draw the symbol based on these
locations.
1.

First, check that the passed in Geometry parameter contains a valid object, then cast it to a Point.
[Visual Basic 6]

If Not TypeOf Geometry Is IPoint Then Exit Sub


Dim pPoint As esriGeometry.IPoint
Set pPoint = Geometry
2.

Transform the Point to device coordinates, using the device context and DisplayTransformation you saved in
SetupDC. Call the CalcCoords function. This function will calculate the location of each of the control points used
by the GDI functions (see the diagram on previous page).
[Visual Basic 6]

Dim lCenterX As Long, lCenterY As Long


Set pPoint = Geometry
FromMapPoint m_pTrans, pPoint, lCenterX, lCenterY
CalcCoords CDbl(lCenterX), CDbl(lCenterY)
3.

Then draw the separate sections of the symbol to the device.


[Visual Basic 6]

m_lOldBrush = SelectObject(m_lhDC, m_lBrushTop)


Chord(m_lhDC, m_pCoords(5).x, m_pCoords(5).y, m_pCoords(6).x, _
m_pCoords(6).y, m_pCoords(4).x, m_pCoords(4).y, m_pCoords(1).x,
m_pCoords(1).y)
...
SelectObject m_lhDC, m_lOldBrush
ResetDC method
Complete the drawing functions by selecting back the old GDI pen and ROP code and releasing other GDI resources in
the ResetDC method.
[Visual Basic 6]

m_lROP2 = SetROP2(m_lhDC, CLng(m_lROP2Old))


SelectObject m_lhDC, m_lOldPen
DeleteObject m_lPen
...
Set m_pTrans = Nothing
m_lhDC = 0
If using the Windows GDI to draw to the display, make sure you reselect the old GDI objects after
drawing.
QueryBoundary method
In the QueryBoundary method you must populate the passed-in Boundary parameter, which is a Polygon, with the
shape of your symbol in map coordinates.
The nonsymmetrical nature of the logo means that it is simpler to calculate the exact shape of the symbol, rather than
approximating a shape. You can create the shape of the logo by working out the radius of the circular section of the
logo (dRad) and the length of the triangular sections of the symbol (dVal).
[Visual Basic 6]

Dim pPtColl As IPointCollection, pSegColl As ISegmentCollection


Dim dVal As Double, dRad As Double
Set pPtColl = pBoundary
Set pSegColl = pBoundary
dRad = dMapSize / 2
dVal = Sqr((dRad * dRad) / 2)
pPtColl.AddPoint CreatePoint(pPoint.x + dVal, pPoint.y - dVal)
pPtColl.AddPoint CreatePoint(pPoint.x - dVal, pPoint.y - dVal)
pPtColl.AddPoint CreatePoint(pPoint.x - dVal, pPoint.y + dVal)
pSegColl.AddSegment CreateCircArc(pPoint, pPtColl.Point(2),pPtColl.Point(0))
QueryBoundary is a client-side storage function; therefore, you should add Point objects to the ISegmentCollection
interface of the passed-in Boundary object. See the 'Coding Interface Members' section of Chapter 2 for advice on
coding client-side storage methods in VB.
ROP2 property
The ROP2 property indicates which type of pen (or Raster OPeration) is used to draw a symbol. The ROP2 code of the

182

device can easily be changed using the GDI functions SetROP2 and GetROP2, but remember to change the ROP2 code
back to its original value in ResetDC, as other symbols will be 'sharing' the same device.
The esriRasterOpCodes enumeration defines the possible ROP2 codes. Changing the ROP2 code can dramatically alter
the appearance of the symbol.

For more information on drawing with different raster operations, search Windows documentation. Windows raster
operation constants correspond to esriRasterOpCodes.
Null transformations and resolution in Draw and QueryBoundary
(converting from map to device units)
As the scalar properties Size, XOffset and YOffset hold values in Points, you must convert from Points to device units
(pixels) before drawing the symbol (for example, during SetupDC), using device coordinates.
You can calculate a device resolution, m_dDeviceRatio, in pixels per Point, using the DisplayTransformation passed to
the SetupDC method.
[Visual Basic 6]

Private Sub SetupDeviceRatio(ByVal hDC As Long, ByVal displayTransform _


As IDisplayTransformation)
If Not displayTransform Is Nothing Then
If displayTransform.Resolution <> 0 Then
m_dDeviceRatio = displayTransform.Resolution / 72
If displayTransform.ReferenceScale <> 0 Then
m_dDeviceRatio = m_dDeviceRatio * _
displayTransform.ReferenceScale / displayTransform.ScaleRatio
End If
End If
SetupDeviceRatio calculates how many pixels on the device equal one printer's Pointthis is used to
transform Size, XOffset, and YOffset from Points to device units. Note that the ReferenceScale of the
Transformation, if present, is also accounted for here.
In some situations your symbol may be required to draw to a device context for which this parameter is nullfor
example, when drawing to the table of contents. In this case, you can get the resolution directly from the screen by
using the GetDeviceCaps Windows API call.
[Visual Basic 6]

Else
If hdc <> 0 Then
m_dDeviceRatio = CDbl(GetDeviceCaps(hdc, LOGPIXELSX)) / 72
Else
m_dDeviceRatio = 1 / (Screen.TwipsPerPixelX / 20)

' 1 Pt = 20 Twips.

End If
End If
Once the device ratio is calculated, Draw can use the FromMapPoint function (see accompanying sample code) to
convert the Geometry the symbol is drawn at from map units into device units.
The SetupDeviceRatio and FromMapPoint function together to transform map units to Points.
Converting from Points to map units
In the QueryBoundary method, you need to convert Size, XOffset, and YOffset from Points to map units to construct a
Geometry in map units representing the boundary of your Symbol.
Add a function called PointsToMap to complete this conversion; if no DisplayTransformation is present, use the value
from SetupDeviceRatio.
[Visual Basic 6]

Private Function PointsToMap(ByVal pDisplayTransform As ITransformation, _

183

ByVal dPointSize As Double) As Double


If pDisplayTransform Is Nothing Then
PointsToMap = dPointSize * m_dDeviceRatio
Else
Dim pTempTransform As IDisplayTransformation
Set pTempTransform = pDisplayTransform
PointsToMap = pTempTransform.FromPoints(dPointSize)
End If
End Function
The PointsToMap function transforms values from Points to map coordinates.
Drawing efficiently
Code the ISymbol methods efficiently, as they may be called frequently. There are a number of issues you could
consider to increase your symbol's drawing efficiency.
A symbol's draw methods may be called frequently; consider the efficiency of your code.

Calculating and storing the shape of the Symbol


The LogoMarkerSymbol calculates the shape and size of the Symbol in two different coordinate spaces: in device
units for ISymbol::Draw and in map coordinates for ISymbol::QueryBoundary and
IMarkerMask::QueryMarkerMask.
Think about the amount of processing each set of calculations will require and which will limit the speed of these
functions. Storing and calculating the shape of the symbol in both map and device coordinates may enable you
to create a more efficient symbol; however, using a single method may make your code simpler and more
maintainable.
Think also about the routines you use to manipulate the shape of your symbol; these may be called frequently.
Therefore, providing a direct mathematical approach may be quicker than the QI's and object creation you may
need to use to convert using the geometrical transformations inside ArcObjects.

Caching the shape of the Symbol


If more than one item is drawn with exactly the same Symbol, the drawing sequence starts with a call to
SetupDC. Then Draw is called once for each item, and finally, ResetDC is called. The diagram below shows the
sequence of calls for a SimpleRenderer and a ClassBreaksRenderer.

It may be most efficient to work out the size and shape of your Symbol once in the SetupDC method, then use
this repeatedly in the Draw method by just changing its location, depending on how you draw your Symbol.

Efficient object creation


Think about how your code will scale when it is used for hundreds of features or elements. For example,
QueryBoundary is called frequently by ArcMap when drawing a FeatureLayer and when drawing elements.
QueryBoundary is also called when displaying the TOC, saving the document, and displaying property pages that
show the Symbol. You should ensure your QueryBoundary routine is efficient enough not to impede these
processes, which may interrupt the user's work flow.
You could see a decrease in your draw times if you instantiate all the objects you need when the Symbol is
instantiated, then reset the values each time.
For example, the QueryBoundsFromGeom function creates new Point objects to build the boundary of the
Symbol.
[Visual Basic 6]

pPtColl.AddPoint CreatePoint(pPoint.x + dVal, pPoint.y - dVal)


pPtColl.AddPoint CreatePoint(pPoint.x - dVal, pPoint.y - dVal)
pPtColl.AddPoint CreatePoint(pPoint.x - dVal, pPoint.y + dVal)
pSegColl.AddSegment CreateCircArc(pPoint, pPtColl.Point(2), PtColl.Point(0))
You could declare Point objects as member variables m_pt1, m_pt2, and m_pt3, instantiate them when the class

184

is initialized, and reuse them in the QueryBoundsFromGeom method.


[Visual Basic 6]

pt1.PutCoords pPoint.x + dVal, pPoint.y - dVal


pt2.PutCoords pPoint.x + dVal, pPoint.y - dVal
pt3.PutCoords pPoint.x + dVal, pPoint.y - dVal
pPtColl.AddPoint pt1
pPtColl.AddPoint pt2
pPtColl.AddPoint pt3
pSegColl.AddSegment CreateCircArc(pPoint, pt3, pt1)
This code can execute approximately 50 percent faster when you repeatedly call QueryGeometry.
Creating and implementing the ILogoMarkerSymbol interface
You need to provide a way to change the colors of the separate sections of the logo design.
Create an interface called ILogoMarkerSymbol, with four read-write properties, ColorLeft, ColorRight, ColorTop, and
ColorBorder. For more information on how you can create a new interface, see Chapter 2, 'Developing Objects'.
The custom ILogoMarkerSymbol interface allows a client to change the colors of the different
sections of the logo.
Implement ILogoMarkerSymbol in the LogoMarkerSymbol coclass. In each property, clone the incoming IColor
parameters and set the appropriate member variable.
[Visual Basic 6]

Private Property Let ILogoMarkerSymbol_ColorBorder(ByVal RHS As esriDisplay.IColor)


Dim pClone As IClone
Set pClone = RHS
Set m_pColorBorder = pClone.Clone
End Property
Implementing IMarkerSymbol
Implementing IMarkerSymbol allows ArcGIS to recognize that your class can be used to symbolize points.
MarkerSymbol properties for the LogoMarkerSymbol. This interface is commonly used by the ArcGIS applications, for
example, when setting Color and Size using the Element Properties dialog box.
Implementing IMarkerSymbol ensures that a Symbol is able to interact with the ArcMap user
interface, for example the Element Properties dialog box
Code the Color property to refer to the predominant color at the top of the logo, by calling the
ILogoMarkerSymbol::ColorTop property.
[Visual Basic 6]

Private Property Get IMarkerSymbol_Color() As esriDisplay.IColor


Dim pLogoMS As ILogoMarkerSymbol
Set pLogoMS = Me
Set IMarkerSymbol_Color = pLogoMS.ColorTop
In the Angle property you can add a check for angles greater than 360 degrees.
[Visual Basic 6]

Private Property Let IMarkerSymbol_Angle(ByVal RHS As Double)


If RHS > 360 Then
RHS = RHS - (Int(RHS / 360) * 360)
End If
m_dAngle = RHS
End Property
Implementing ISymbolRotation
If you want your symbol to be able to adjust itself to a rotated map display, implement ISymbolRotation. Although it is
not essential to implement this interface, it requires little extra coding, as you should have already added Symbol
rotation code to allow for the IMarkerSymbol::Angle property.
When you rotate the symbol for drawing, simply subtract the Map rotation angle from the IMarkerSymbol::Angle. You
can get the Map rotation value from the DisplayTransformation passed in SetupDC:
[Visual Basic 6]

dAngle = 360 - (m_dAngle - m_dMapRotation)


RotateWithTransform is True by default for existing ArcGIS symbols.
ISymbolRotation allows a Symbol to work with the Data Frame tools in ArcMap, rotating with the

Map.

185

Implementing IMapLevel
IMapLevel is commonly used by the ArcMap Advanced Drawing Options to draw joined and merged symbols, most
commonly those used to draw cased roads. It is simple to implement, as you only need to store a Long value in the
read-write MapLevel property
[Visual Basic 6]

Private Property Let IMapLevel_MapLevel(ByVal RHS As Long)


m_lMapLevel = RHS ' Store passed in value in a global variable.
End Property
This value will be used when your symbol is used in a MultiLayerMarkerSymbol, when the Advanced Drawing Options
indicate symbols must be drawn joined and merged.

IMapLevel allows a symbol to take part in the ArcMap Advanced Drawing Options.
Implementing IMarkerMask
IMarkerMask is used to draw a mask around a symbol. The QueryMarkerMask method should populate the Boundary
parameter with the shape of the symbol if drawn at the specified Geometry. The shape needs to be in map units, as it
will be passed to the ISymbol::Draw method of an IFillSymbol by ArcMap.
By implementing IMarkerMask, you allow the framework to draw a mask area around your symbol.
First ensure the Boundary is empty, then use the same technique you used in ISymbol::QueryBoundary to populate
Boundary.
[Visual Basic 6]

Boundary.SetEmpty
QueryBoundsFromGeom hDC, Transform, Boundary, Geometry
Unlike QueryBoundary, however, QueryMarkerMask requires a Simple geometry, so simplify the geometry before
returning.
[Visual Basic 6]

Dim pTopo As ITopologicalOperator


Set pTopo = Boundary
If Not pTopo.IsKnownSimple Then
If Not pTopo.IsSimple Then
pTopo.Simplify
End If
End If
Implementing IPropertySupport
IPropertySupport can be implemented in VC++ and is used to apply an object to one or more of the symbol's
properties. It is a generic interface, which can be used by a client without the client needing to know the exact nature
of the underlying class.
IPropertySupport is an optional interface. It cannot be implemented in VB6.
In the Applies method, you should assess the incoming object reference pUnk, to see if it can be applied to a property
of your class.
[Visual C++]

STDMETHODIMP CLogoMarkerSymbol::Applies(LPUNKNOWN pUnk,VARINT_BOOL *Applies)


{
if (!Applies)
return E_POINTER;
*Applies = VARIANT_FALSE;
IColorPtr ipColor(pUnk);
ILogoMarkerSymbolPtr ipLogo(pUnk);
if (ipColor != NULL && ipLogo != NULL)
*Applies = VARIANT_TRUE;
return S_OK;
}
In the CanApply method, check if the object can be applied at the particular moment the method is called; a more

186

complex class may involve checking the internal state of the class). In the case of the LogoMarkerSymbol, the result
does not depend on any state, so you can delegate the call to Applies.
In the Current property, check the incoming object referenceif it can be applied to any of the properties of the class,
set the pUnk pointer to the current value of that property.
[Visual C++]

STDMETHODIMP CLogoMarkerSymbol::get_Current(LPUNKNOWN pUnk, LPUNKNOWN *currentObject)


{
IColorPtr ipColor(pUnk);
if (ipColor)
{
IColorPtr ipCurrentColor;
get_Color(&ipCurrentColor);
ipCurrentColor.QueryInterface(IID_IUnknown, (void**)currentObject);
return S_OK;
}
...
}
In the Apply method, set the incoming object as the appropriate member of your symbol class. Note in the code below
that the incoming object may be an instance of the LogoMarkerSymbol class itself, in which case the values of the
incoming object are assigned to the class member by using the IClone::Assign method.
[Visual C++]

STDMETHODIMP CLogoMarkerSymbol::Apply(LPUNKNOWN NewObject, LPUNKNOWN *oldObject)


{
IColorPtr ipColor(NewObject);
if (ipColor)
{
get_Current(NewObject, oldObject);
put_Color(ipColor);
return S_OK;
}
ILogoMarkerSymbolPtr ipSymbol(NewObject);
if (ipSymbol)
{
get_Current(NewObject, oldObject);
IClonePtr ipClone(NewObject);
Assign(ipClone);
return S_OK;
}
return E_FAIL;
}
To be consistent with core symbols, you should at least apply an IColor object to the IMarkerSymbol::Color property,
although you can extend this to allow the setting of any of your properties.
Initializing members
Add a private routine to the LogoMarkerSymbol class to initialize the member variables. Call this function from the
class initialize.
[Visual Basic 6]

Private Sub InitializeMembers()


m_lhDC = 0
Dim pColor As IColor, pClone As IClone
Set pColor = New RgbColor
Set pClone = pColor
pColor.RGB = RGB(255, 0, 0)
Set m_pColorTop = pClone.Clone
...
m_lROP2 = esriROPCopyPen
...
m_bRotWithTrans = True
End Sub

187

Placing the initialization code in a separate function enables you to reset the LogoMarkerSymbol to default values at
any point, which is particularly useful when implementing persistence.
Implementing cloning and persistence
Cloning and persistence are essential functions for any symbol. Every time a reference to a symbol is passed to a
property page, the symbol object is cloned. This allows any changes made to the symbol to be discarded and also
allows the change to be added to the Undo/Redo stack in ArcMap. Every time a map document is saved, all the
symbols applied to features and graphic elements are persisted. Add a standard implementation of persistence and
cloning for the LogoMarkerSymbol example. See Chapter 2, 'Developing Objects', for more information on cloning and
persistence.
In the IPersistVariant::Save method, save the persistence version number first, then each required member of the
class.
[Visual Basic 6]

Private Sub IPersistVariant_Save(ByVal Stream As esriSystem.IVariantStream)


Stream.Write m_lCurrPersistVers
Stream.Write m_lROP2
Stream.Write m_dSize
Stream.Write m_dXOffset
...
In IPersistVariant::Load check the persistence version number first. Call the InitializeMembers function to set default
values into the symbol, before reading values from the Stream, in the same order they were saved to set the member
variables.
[Visual Basic 6]

Private Sub IPersistVariant_Load(ByVal Stream As IVariantStream)


Dim lSavedVers As Long
lSavedVers = Stream.Read
If (lSavedVers > m_lCurrPersistVers) Or (lSavedVers <= 0) Then
Err.Raise E_FAIL
Exit Sub
End If
InitializeMembers
If lSavedVers = 1 Then
m_lROP2 = Stream.Read
m_dSize = Stream.Read
m_dXOffset = Stream.Read
...
In the IClone::IsEqual method, you may decide that the source and other symbols are equal if the RGB property
values of IColor members are equivalent, instead of QIing to IClone on the color class and checking its IsEqual
member in turn.
[Visual Basic 6]

Private Function IClone_IsEqual(ByVal other As IClone) As Boolean


IClone_IsEqual = True
If Not other Is Nothing Then
If TypeOf other Is ILogoMarkerSymbol Then
Dim pSrcLogoSym As ILogoMarkerSymbol
Dim pRecLogoSym As ILogoMarkerSymbol
Set pSrcLogoSym = other
Set pRecLogoSym = Me
IClone_IsEqual = IClone_IsEqual And _
(pRecLogoSym.ColorBorder.RGB = pSrcLogoSym.ColorBorder.RGB)
IClone_IsEqual = IClone_IsEqual And _
(pRecLogoSym.ColorLeft.RGB = pSrcLogoSym.ColorLeft.RGB)
...

Symbol Property Pages


All core symbols have a property page that is displayed in the Symbol Editor dialog box. This allows the user to edit
the properties of the symbol in the user interface. Some more complex symbols have multiple property pages,
displayed as separate tabs in the dialog box.
It is not absolutely essential for a custom Symbol coclass to have an accompanying property page, although it is
recommended that you do. If you only intend a Symbol to be applied and edited programmatically, you need not
implement a property page, but users may be confused when they try to change the properties of the Symbol in the
editor.

188

Create a LogoMarkerPropertyPage coclass as shown in the accompanying sample code, and register the class to the
Symbol Property Pages component category. Follow the general rules for property pages given in Chapter 2,
'Developing Objects'. Additionally, the following section highlights particular details relevant to this implementation, in
particular the implementation of ISymbolPropertyPage.
Add a separate Form class to provide the UI component of the property page. You will link the
LogoMarkerPropertyPage coclass to the form by adding public properties to the form.

The LogoMarkerPropertyPage is displayed in the Symbol Editor dialog box.


Note that when you open the Symbol Editor for a LogoMarkerSymbol, the dialog box also has a Mask
property pagethis is displayed automatically if the current Symbol implements IMarkerMask.
Implementing property page interfaces for the LogoMarkerPropertyPage
The interfaces implemented on the LogoMarkerPropertyPage are dependent on the development environment, as
described in Chapter 2. In either case, the basic structure of the property page class is similar. In the Applies method,
return True if you receive a LogoMarkerSymbol.
[Visual Basic 6]

For i = 0 To Objects.Count - 1
Set pObj = Objects.Next
If TypeOf pObj Is ILogoMarkerSymbol Then
Set pAppliesClone = pObj
IComPropertyPage_Applies = True
Exit Function
End If
Next i
Add a property named LogoMarkerSymbol to the Form class. In the SetObjects method, check that you receive a
LogoMarkerSymbol object, then set the LogoMarkerSymbol property of the Form to this object.
[Visual Basic 6]

Dim pObj As Variant, i As Long


Objects.Reset
For i = 0 To Objects.Count - 1
Set pObj = Objects.Next
If Not pObj Is Nothing Then
If TypeOf pObj Is ILogoMarkerSymbol Then
Set m_pObjectLogoMarker = pObj

' hold on to the symbol

End If
End If
Next i

189

If Not m_pObjectLogoMarker Is Nothing Then


Set m_frmPage.LogoMarkerSymbol = m_pObjectLogoMarker
m_frmPage.UpdateControls
m_frmPage.IsPageDirty = False
End If
Add a property called IsPageDirty to the form, and link this to the IComPropertyPage::IsPageDirty property of the
LogoMarkerPropertyPage class.
This allows events from controls on the Form to directly change properties of the Symbol. The changes can then be
seen in the Preview box after calling IComPropertyPageSite::PageChanged to refresh the dialog box.
Implementing ISymbolPropertyPage
The Size, XOffset, and YOffset properties of a MarkerSymbol are returned and set onto the LogoMarkerSymbol as a
printer's Point measurement. However, users may prefer to define the properties of a symbol using a different system
of measurement, for example, centimeters or millimeters.
If the ability to use different systems of measurement were encapsulated in the Symbol coclasses themselves, this
would not only mean each class contained similar conversion code, but it would complicate the use of the classes.
Therefore, this functionality is added at the property page level by implementing the specialist ISymbolPropertyPage
interface.
At the top right hand side of the Symbol Editor dialog box, you can see the Units combo box. When the user changes
the Units selection, the property sheet to which the page belongs (in this case the Symbol Editor) calls the active
property page to tell it what type of units the user has selected by setting the ISymbolPropertyPage::SymbolUnits
property.

By implementing ISymbolPropertyPage, you can allow a property page to react correctly to changes
in the Units combo box of the containing property sheet.
When creating a symbol property page, you must provide the ability to convert from the selected units of
measurement to Points.
Add a read-write property to the form called Units, which stores the selected units value in a member variable of the
Form. When this property is changed, call the UpdateControls method.
UpdateControls should account for a change in Units by converting the values shown in the Size, XOffset, and YOffset
controls to the currently selected unit typethis is because these properties are always stored internally in Points.
[Visual Basic 6]

txtSize.Text = PointsToUIValue(m_pMarker.Size)
txtXOffset.Text = PointsToUIValue(m_pMarker.XOffset)
txtYOffset.Text = PointsToUIValue(m_pMarker.YOffset)
Then add the PointsToUI procedure to convert values from Points to the current display unitsit returns a formatted
string, in the currently selected units, which can be displayed in the Size, XOffset, and YOffset controls.
[Visual Basic 6]

Private Function PointsToUIValue(ByVal dValue As Double) As String


Select Case m_lUnits
Case esriPoints
PointsToUIValue = FormatNumber(dValue, 2)
Case esriInches
PointsToUIValue = FormatNumber(dValue / 72#, 4)
Case esriCentimeters
PointsToUIValue = FormatNumber((dValue / 72#) * 2.54, 2)
Case esriMillimeters
PointsToUIValue = FormatNumber((dValue / 72#) * 24.5, 2)
End Select
End Function
See Also Customizing the Display, Creating custom symbols, and Vertex Line Symbol Example.

190

Vertex Line Symbol Example


Object Model Diagram

Example Code Click here


Description This project provides a custom symbol to draw a line and its vertices. Simple custom functionality is
provided to alter both the symbol used to draw the basic shape of the line, and the symbol used to draw its vertices. A
property page is also provided to allow users to edit the properties of the symbol using the user interface.
Design Coclass VertexLineSymbol is a subtype of the LineSymbol abstract class, with an accompanying property page
coclass.
License required ArcView or above
Libraries Framework, Display, DisplayUI, Geometry, and System
Languages Visual Basic (some restrictions)
Categories Line Symbols, ESRI Property Pages, and Line Property Pages
Interfaces ISymbol, ILineSymbol, IMapLevel, IDisplayName, IPropertySupport, IClone, IPersist, IComPropertyPage,
IPropertyPageContext, and ISymbolPropertyPage
How to use
1.

If using VB, register VertexLineSymbolVB.dll and double-click the VertexLineSymbolVB.reg file to register
to component categories.

2.

Open ArcMap and add a feature layer with line features or add a line graphic element. Open the Symbol
Selector for the item.
For a line feature layer, right-click the layer in the ArcMap table of contents, click Properties, and in the
Layer Properties dialog box, click the Symbology tab. The Single Symbol renderer should be selected by
default. Click the Symbol button to show the Symbol Selector.
For a line element, right-click the element and click Properties. In the Properties dialog box, make sure the
Symbol tab is selected and click the Change Symbol button.

3.

In the Symbol Selector dialog box, click the Properties button to display the Symbol Editor.

4.

In the Symbol Editor dialog box, pull down the Type list box and click Vertex Line Symbol.
You can now set the properties of a VertexLineSymbol. Click OK to select the symbol and return to the
Symbol Selector.

Case for a custom Line symbol


Sometimes when using line feature or graphics, it helps to be able to clearly see the vertex points of the line.
For example, when editing a feature layer in ArcMap, the vertices of a line are highlighted with separate symbols. This
type of display helps particularly on lines with numerous vertices, and also those with curved segments, as it is more
difficult to identify the location of the vertices on lines with parametric curved segments.

191

No existing line symbol can be used to display a line or polygon feature by highlighting its vertices. (Marker symbols
can be added to a line by using LineDecorations, but such decorations display at certain measurements along the line
and cannot be used to draw the vertices of a line.)
As no symbol coclass provides the ability to display a line as required, you will create a custom line symbol to meet the
requirements.
This example demonstrates how to construct a custom symbol to draw lines and to highlight each
vertex of the line.

Creating a subtype of LineSymbol


All types of custom symbols are based on the Symbol abstract class, the details of which can be found in the previous
example, LogoMarkerSymbol. Refer to this example for general details of how to create a custom symbol.
Additional issues relevant to LineSymbols in particular are discussed throughout this section.

In the Display object model diagram, you can see that each coclass for drawing line features also inherits from the
LineSymbol abstract class.
Therefore, to create a LineSymbol, you should implement ILineSymbol, IMapLevel, and IPropertySupport. All
LineSymbols also implement IDisplayName, which provides a string description of each type of symbol and is used in
the Symbol Properties Editor dialog box.
By looking at the other LineSymbols, you can see that many of them also implement ICartographicLineSymbol, which
controls the line cap and join styles, and the miter limit with which a line is drawn. ICartographicLineSymbol is not an
appropriate interface to implement in this example, as cartographic lines can be used anyway and you will reuse
existing LineSymbols in your custom symbol.
IPropertySupport cannot be implemented in VB
Note that it is not possible to implement IPropertySupport in VB. However, this will not affect the main functionality
ArcMap does not assume that this interface is implemented, but may check for its presence on any LineSymbol.
The discussion for this example centers on the VB example project, as the approach taken is the same, regardless of
development environment. The implementation of IPropertySupport is discussed in the previous example,
LogoMarkerSymbol. The same principles can be applied for the VertexLineSymbol.

Creating the VertexLineSymbol

To solve the requirements of this example, you will create a subtype of LineSymbol, called VertexLineSymbol,
implementing ISymbol, ILineSymbol, IMapLevel, and IDisplayName, as well as the standard interfaces for cloning and
persistence.
To create a flexible class, with maximum reuse of existing code, your VertexLineSymbol will draw a line by using any
LineSymbol, then draw the vertices of the line using any MarkerSymbol. To add this custom functionality, you will also
create and implement a custom interface, IVertexLineSymbol.

192

Techniques for drawing


In the previous example, LogoMarkerSymbol, a variety of techniques that can be used for the actual drawing were
discussed, and the approach used was to call Windows GDI functions.
In this example, however, you will reuse ArcGIS LineSymbols and MarkerSymbols and can, therefore, make use of the
ISymbol::Draw method to perform the drawing, which reduces the complexity of the code you need to write.
To draw the VertexLineSymbol, you will use the existing LineSymbols and MarkerSymbols.
In the VertexLineSymbol class, declare member variables to hold references to the ISymbol interface of the
LineSymbol and MarkerSymbol which you will use to perform the drawing.
[Visual Basic 6]

Private m_pSymLine As ISymbol


Private m_pSymMarker As ISymbol
You will also need references to the more specific ILineSymbol and IMarkerSymbol interfaces so you can change the
properties of these symbols.
[Visual Basic 6]

Private m_pLineSym As ILineSymbol


Private m_pMarkerSym As IMarkerSymbol
Create a function called InitializeMembers, and in this function, create the default symbols with which your
VertexLineSymbol will draw. This is also a good opportunity to initialize any other member variables.
[Visual Basic 6]

Private Sub Class_Initialize()


m_lhDC = 0
Set m_pLineSym = New SimpleLineSymbol
Set m_pMarkerSym = New SimpleMarkerSymbol
Set m_pSymLine = m_pLineSym
Set m_pSymMarker = m_pMarkerSym
End Sub
Call this function from your class initialization code.
Implementing ISymbol
In the SetupDC method, you only need to store as member variables the references to the parameters passed in (hDC
and displayTransformation), which will be used later in the Draw method.
[Visual Basic 6]

Private Sub ISymbol_SetupDC(ByVal hdc As esriSystem.OLE_HANDLE, _


ByVal Transformation As esriGeometry.ITransformation)
Set m_pTrans = Transformation
m_lhDC = hdc
As you are not using GDI functions or other drawing libraries, you do not need to set up any objects for drawing.
A general discussion of how to implement the ISymbol interface can be found in the previous
example, LogoMarkerSymbol.
In the Draw method, check the passed in Geometry parameter contains a valid object before drawing the basic shape
of the line. Call the SetupDC method of the ILineSymbol member variable to set the LineSymbol as the current symbol
for the display, passing in the device context handle and transformation you received in the SetupDC method of your
custom symbol. Then call Draw, passing in the Geometry parameter, and finally call ResetDC.
[Visual Basic 6]

Private Sub ISymbol_Draw(ByVal Geometry As esriGeometry.IGeometry)


If Geometry Is Nothing Then Exit Sub
m_pSymLine.SetupDC m_lhDC, m_pTrans
m_pSymLine.Draw Geometry

193

m_pSymLine.ResetDC
To draw the individual vertices of the line, QI for the IPointCollection interface of the Geometry parameter. Set the
MarkerSymbol as the current symbol for the display in the same way as you did previously for the LineSymbol.
[Visual Basic 6]

If TypeOf Geometry Is IPointCollection Then


Dim ptColl As IPointCollection
Set ptColl = Geometry
m_pSymMarker.SetupDC m_lhDC, m_pTrans
Then iterate through the IPointCollection, passing each individual Point to the ISymbol::Draw method of the
MarkerSymbol. Finally, call ResetDC on the MarkerSymbol.
[Visual Basic 6]

Dim i As Integer
For i = 0 To (ptColl.PointCount - 1)
m_pSymMarker.Draw ptColl.Point(i)
Next i
m_pSymMarker.ResetDC
End If
To perform the drawing, simply draw the basic line shape, then iterate each vertex of the line,
drawing each in turn.
In the ResetDC method, simply release the transformation, and set the device context handle back to zero.
[Visual Basic 6]

Set m_pTrans = Nothing


m_lhDC = 0
You must ensure your calls to the LineSymbol and MarkerSymbol SetupDC, Draw, and ResetDC methods are made
inside the ISymbol::Draw method of your class, as these methods can only be called when the device is currently in
drawing mode.
In some situations, the Transformation parameter received in SetupDC may be nullfor example, when drawing to the
table of contents. This will not affect your Draw method, as you simply pass the parameter on to another Symbol,
which will account for the null transformation.
If the ROP2 property of your VertexLineSymbol is set, change the ROP2 properties of both the LineSymbol and
MarkerSymbol members.
[Visual Basic 6]

Private Property Let ISymbol_ROP2(ByVal RHS As esriDisplay.esriRasterOpCode)


m_pSymLine.ROP2 = RHS
m_pSymMarker.ROP2 = RHS
End Property
In the previous example, the QueryBoundary method was relatively complex to calculate, as the shape of the symbol
was stored in device coordinates, which required a conversion to Map coordinates to return the boundary. However, in
the VertexLineSymbol example, you could calculate a boundary more simply, as you always work in Map coordinates.
One method would be to create the shape of the boundary by QIing to the IPointCollection interface of both the
Geometry and Boundary parameters.
[Visual Basic 6]

Dim pSegs_From As ISegmentCollection, pSegs_To As ISegmentCollection


Set pSegs_From = Geometry
Set pSegs_To = Boundary
Then add the vertices from the Geometry to the Boundary. Don't forget that the result of QueryBoundary must be
closed (the Geometry you received may not have been closed). Last, ensure the Boundary is simplified.
[Visual Basic 6]

pSegs_To.AddSegmentCollection pSegs_From
Boundary.Close
Dim pTopoBoundary As ITopologicalOperator
Set pTopoBoundary = Boundary
pTopoBoundary.Simplify
However, if you investigate the value of QueryBoundary for existing symbols, FillSymbols return a Boundary that
follows the shape drawn, but LineSymbols actually return a rectangular Boundary polygon. This is actually the
Envelope of the Geometry when drawn with the LineSymbol, accounting for the Width.
Here you can see the Boundary returned from various symbols; the symbols are purple, and the red
hatched area shows the Boundary.

194

For a QueryBoundary implementation closer to the behavior of the existing symbols, you can make use of the
QueryBoundary method on the existing SimpleLineSymbol class.
First, declare a member variables to store a SimpleLineSymbol, so you do not have to instantiate the symbol each
time QueryBoundary is called.
[Visual Basic 6]

Private m_pQBLine As ISimpleLineSymbol


Instantiate the new SimpleLineSymbol in your class initialization code.
[Visual Basic 6]

Set m_pQBLine = New SimpleLineSymbol


In the QueryBoundary method, first ensure the Boundary passed in is valid, and clear any preexisting shape by calling
SetEmpty.
[Visual Basic 6]

If Not Boundary Is Nothing Then


Boundary.SetEmpty
To ensure the Boundary includes the vertex MarkerSymbols, calculate which is greater: the Width of the LineSymbol or
the Size of the MarkerSymbol. Then set the Width of the SimpleLineSymbol, m_pQBLine, to this value, and call its
QueryBoundary method, passing in the same parameters you received.
[Visual Basic 6]

If m_pLineSym.Width > m_pMarkerSym.Size Then


m_pQBLine.Width = m_pLineSym.Width
Else
m_pQBLine.Width = m_pMarkerSym.Size
End If
Dim pQBSym As ISymbol
pQBSym.QueryBoundary hdc, displayTransform, Geometry, Boundary
Creating and implementing the IVertexLineSymbol interface
You need to provide a way to change the LineSymbol and MarkerSymbol you use to draw the VertexLineSymbol.
Create an interface called IVertexLineSymbol, with two read-write properties, LineSymbol and VertexSymbol. For more
information on how you can create a new interface, see Chapter 2, 'Coding Interfaces'.
Implement IVertexLineSymbol in the VertexLineSymbol coclass. When each property is set, clone the incoming
Symbol, and store the reference as the appropriate member variable. Also, ensure the reference to the ISymbol
interface, m_SymMarker or m_pSymLine, is also updated.
[Visual Basic 6]

Private Property Let IVertexLineSymbol_VertexSymbol(ByVal RHS _


As IMarkerSymbol)
If Not RHS Is Nothing Then
Set m_pMarkerSym = CloneMe(RHS)
Set m_pSymMarker = m_pMarkerSym
End If
End Property
IVertexLineSymbol is a custom interface defined to allow clients to change the appearance of a VertexLineSymbol.
Implementing ILineSymbol
ILineSymbol provides basic LineSymbol properties, allowing the Width and Color of the LineSymbol to be altered. This
interface is commonly used in ArcGIS, for example by the Element Properties dialog box.
Code both of these properties to call only the LineSymbol of the VertexLineSymbol, leaving the MarkerSymbol
unaffected.
[Visual Basic 6]

Private Property Let ILineSymbol_Color(ByVal RHS As esriDisplay.IColor)


If Not RHS Is Nothing Then

195

m_pLineSym.Color = RHS
End If
End Property
Private Property Let ILineSymbol_Width(ByVal RHS As Double)
If RHS > 0 Then
m_pLineSym.Width = RHS
End If
End Property
Allow the ILineSymbol interface to alter the color and width of the contained LineSymbol.
Implementing IMapLevel, IDisplayName, and IPropertySupport
Implement the IMapLevel and IDisplayName interfaces. From IDisplayName::DisplayName, return 'Vertex Line
Symbol'. If you are working in VC++, you can also implement the optional IPropertySupport interface if you require.
For more details of the implementation of IMapLevel, IDisplayName, and IPropertySupport, refer to the previous
example, LogoMarkerSymbol; the implementations follow the same principles as shown previously.
Implementing cloning and persistence
As discussed in the previous example, cloning and persistence are essential for any Symbol. You should provide a
standard implementation of IClone and either IPersistVariant or IPersist and IPersistStreamsee Chapter 2 for more
information on cloning and persistence.
The only member variables that you need to persist are the LineSymbol and MarkerSymbol with which the
VertexLineSymbol is drawn, and the current value of IMapLevel::MapLevel. The current persistence version number
should also be written to the stream, allowing for backward compatibility.
[Visual Basic 6]

Stream.Write m_lCurrPersistVers
Stream.Write m_pLineSym
Stream.Write m_pMarkerSym
Stream.Write m_lMapLevel
When you Load a VertexLineSymbol from a stream, first call the InitializeMembers function you created earlier to set
default values for the VertexLineSymbol. Next, set the member variables m_pLineSym and m_pMarkerSym from the
stream. Then set the member variables m_pSymLine and m_pSymMarker to refer to the newly loaded objects.
[Visual Basic 6]

Private Sub IPersistVariant_Load(ByVal Stream As IVariantStream)


Dim lSavedVers As Long
lSavedVers = Stream.Read
If (lSavedVers > m_lCurrPersistVers) Or (lSavedVers <= 0) Then
Err.Raise E_FAIL
Exit Sub
Else
InitializeMembers
If lSavedVers >= 1 Then
Set m_pLineSym = Stream.Read
Set m_pMarkerSym = Stream.Read
Set m_lMapLevel = Stream.Read
Set m_pSymLine = m_pLineSym
Set m_pSymMarker = m_pMarkerSym
End If
End If
End Sub
Next, you can create a property page to accompany your custom line symbol.

Symbol Property Pages


Each symbol has a property page, displayed in the Symbol Editor dialog box, which allows the user to edit the
properties of the symbol in the user interface. Symbol property pages are discussed in the previous example,
LogoMarkerSymbol.

196

To complete the VertexLineSymbol example, you will create a VertexLinePropertyPage coclass, which implements the
ISymbolPropertyPage interface, as well as the standard property page interfaces. You will register the property page
coclass to the Symbol Property Pages component category.
You will create a separate Form class to provide the GUI component of the property page, and link this Form to the
VertexLinePropertyPage coclass by a number of properties.

The VertexLinePropertyPage follows both the design used in the previous example and the general rules for property
page implementation; see Chapter 2 for more information.
The VertexLinePropertyPage allows a user to alter the properties of a VertexLineSymbol by using the
ArcGIS UI.
Implementing property page interfaces for a VertexLinePropertyPage
In the VertexLinePropertyPage coclass, implement either IComPropertyPage and IPropertyPageContext if you are
working in VB or IPropertyPage and IPropertyPageContext if working in VC++.
You will also implement ISymbolPropertyPage, regardless of the development environment.
In the Applies method of IPropertyPageContext or IComPropertyPage, return True if you receive a VertexLineSymbol.
[Visual Basic 6]

For i = 0 To Objects.Count - 1
Set pObj = Objects.Next
If TypeOf pObj Is VertexLineSymbol Then
Set pAppliesClone = pObj
IComPropertyPage_Applies = True
Exit Function
End If
Next i
Add a property named VertexLineSymbol to the Form class. In the SetObjects method of IPropertyPage or
IComPropertyPage, check that you receive a VertexLineSymbol object, then set the VertexLineSymbol property of the
Form to this object.
[Visual Basic 6]

Dim pObj As Variant, i As Long


Objects.Reset
For i = 0 To Objects.Count - 1
Set pObj = Objects.Next
If Not pObj Is Nothing Then
If TypeOf pObj Is VertexLineSymbol Then
Set m_pObjectVertexLine = pObj

' hold on to the symbol

End If
End If

197

Next i
If Not m_pObjectVertexLine Is Nothing Then
Set m_frmPage.VertexLineSymbol = m_pObjectVertexLine
m_frmPage.UpdateControls
m_frmPage.IsPageDirty = False
End If
The SetObjects method of the property page should check that one of the objects received is a
VertexLineSymbol.
Add a property called IsPageDirty to the form, and link this to the IComPropertyPage::IsPageDirty property of the
VertexLinePropertyPage coclass.
This allows events from controls on the Form to directly change properties of the Symbol. The changes can then be
seen in the Preview box, after calling IComPropertyPageSite::PageChanged to refresh the dialog box.
Implementing ISymbolPropertyPage
The details of ISymbolPropertyPage and its use are discussed previously in the LogoMarkerSymbol example.
For the VertexLinePropertyPage, you should implement ISymbolPropertyPage in a similar way. First, add a read-write
property to the Form called Units. When this property is changed, call the UpdateControls method.
The UpdateControls method must account for the units of measurement currently selected in the user interface, when
setting the Width of the VertexLineSymbol's LineSymbol or the Size of the VertexLineSymbol's MarkerSymbol.
[Visual Basic 6]

txtWidth.Text = PointsToUIValue(m_pLineSym.Width)
txtSize.Text = PointsToUIValue(m_pVertexLine.VertexSymbol.Size)
The PointsToUIValue function is the same as that shown previously for the LogoMarkerSymbol.
See Also Customizing the Display, Creating custom symbols, and Logo Marker Symbol Example.

Creating other kinds of custom symbols


The examples in this chapter show two different approaches to creating a custom symbol: inheriting from different
abstract classes, and using different techniques to perform the drawing. These are, of course, only two examples.
There is potential for innumerable different custom symbols.

Fill symbols
You can create a custom fill symbol if required by implementing at least the ISymbol, IFillSymbol, and the persistence
and cloning interfaces. You may also want to implement IMapLevel, IDisplayName, and IPropertySupport, if
appropriate.
If using an external library to draw polygons in particular, pay proper attention to complex geometries, such as selfintersecting, donut, or multipart geometries. These types of geometries may result in unexpected effects when
drawing, as the library (for example, Windows GDI functions) may define the structure of complex geometries in a
different way than that used by ArcGIS geometries.

Text symbols
Text is complex to place accuratelyit is not expected that you will need to create custom TextSymbols. If you need
to implement ITextSymbol, consider that your text should render correctly not just with basic appearance, but when
drawing splined text, and text with different alignment, spacing, and so on. The display of right-to-left text should also
be considered.

Chart symbols
A custom ChartSymbol can be created and applied to the existing ChartRenderer by implementing ISymbol;
IChartSymbol; IMarkerSymbol; and ISymbolArray; and, optionally, I3DChartSymbol, IMarkerBackgroundSupport, and
IPropertySupport.
The integration, which can be achieved with the existing ChartRenderer object and its user interface components, is
not as high as can be achieved by other custom symbols. The ChartRenderer and associated UI are both tightly
integrated with the existing classes of chart symbol; the list of available ChartSymbols is predefined, not found in a
component category. A custom ChartSymbol may be applied to an existing ChartRenderer programmatically and set
up as required; however, if the layer symbology is then edited, the setup will not be reproducible or editable in the UI.
It is possible to improve the integration of the existing UI and your symbol by implementing one of the existing
ChartSymbol interfaces: IBarChartSymbol, IPieChartSymbol, or IStackedChartSymbol. In this way you can 'piggyback'
on the existing UI. Once set to a chart renderer, the existing UI can be used to alter the properties of your custom
ChartSymbol via these existing interfacesimplement whichever has the properties closest to your requirements. The
UI may not behave exactly as expected, as you are not providing exactly the symbol expected by the renderer.
Alternatively, you can create a custom renderer designed to apply your custom ChartSymbol. You may find it helps to
contain an instance of an existing ChartRenderer to provide a framework for the new renderer. The disadvantage of
this approach is the extra work you will need to do, particularly in providing the UI required to allow users to set up
the renderer and in the renderer drawing code.

198

See Also Logo Marker Symbol Example, Vertex Line Symbol Example, and Creating custom symbols.

Custom feature renderers


A feature renderer is an object that is used to draw feature layers. There are several standard feature renderers, for
example, the SimpleRenderer, the ClassBreaksRenderer and the DotDensityRenderer. If none of the standard
renderers satisfy your requirements, and you want complete control over the way features are drawn, you may find it
useful to implement your own custom feature renderer.
Custom feature renderers give you complete control over the way features are drawn. Note that this
section covers custom feature renderers, but not raster renderers.
There are several samples of custom feature renderers in the ArcGIS Developer Help; try looking under Renderers in
the index. As an indication of what custom renderers can do, some of the samples are shown below:

Produce bivariate representations of a feature layer that go beyond the functionality of the standard biunique value
renderer. In the picture, state capitals are symbolized by population and elevation above sea level (Bivariate
Renderers).

Show measures or z-values at vertices of a line layer (MZRenderer).

Show slivers between polygons with a special symbol (Sliver Polygon Renderer).

Symbolize network junctions with a count of how many network edges meet at the junction (Valence Renderer).
There are often alternatives to implementing a custom feature renderer. First, the existing standard renderers support
a wide variety of ways to draw data. Many difficult drawing or symbology requirements can be achieved by
manipulating the properties of a standard renderer with ArcObjects or the ArcGIS UI. Second, it pays to have a strong
working knowledge of the ArcMap symbol model. Many problems can be addressed by using a symbol with its
properties set in a specific way. In particular, multilayer symbols can produce many advanced effects.
Make sure the existing renderers and symbol properties cannot solve your problem before
implementing a custom renderer.
When the data you need to symbolize does not have an attribute that specifically meets your symbolization needs, you
should consider adding a new attribute and calculating or programmatically deriving values. For example, consider the
four-color map problem (see the ArcGIS Developer Help for a sample). It would be too slow if the renderer was
responsible for figuring out which color to draw each feature each time the map gets drawn.

199

Adding a symbology attribute to the data can be a lot more efficient than a custom renderer if
complex symbology requirements only need to be calculated once.
By creating a new field, and calculating its values once and for all, the need for a custom renderer is eliminated
because the standard unique values renderer can now be used on this new field. In fact, this allows ArcMap to render
the data in the fastest way it possibly can. Incidentally, a useful tip is that ArcMap renders data based on an integer
field faster than it would if the field were of a text data type. This is particularly true for ArcSDE geodatabases, since
less data has to be interpreted and transferred over the network.
A custom layer may be an alternative to a custom feature renderer. In particular, a custom layer provides more
complete control over the ArcMap user interface. A custom renderer may be incompatible with some of the standard
user interface facilities for a layer. For example, if the renderer displays the features away from their true locations,
the selection tools will not work correctly. In this case it may be more appropriate to implement a custom layer. Note
that the ILayer::Draw method provides control over how the layer is displayed. Custom layers are generally a bigger
undertaking to implement than custom renderers. For more about custom layers, see Chapter 4, 'Creating
Cartography'.
Custom layers provide more control over the ArcMap user interface than custom renderers.
A custom feature is another alternative to a custom renderer, though the renderer is nearly always a more efficient
solution and also one that is easier to implement. Moreover, data based on custom features can be difficult to share as
the implementation DLL becomes an integral part of the data. The IFeatureDraw interface on a custom feature
provides control over how the feature is displayed. IFeatureDraw::Draw is called by the standard renderers for each
feature. A disadvantage of implementing a custom feature like this is that you have less control over the drawing loop,
and this may force you into redundant calculations. For more discussion, see the information on custom features in
Chapter 8, 'Extending the Geodatabase'.
Custom features provide control over how features are drawn; however, they are normally less
efficient and harder to implement than custom feature renderers.
With custom features, unlike custom renderers, the link between the feature and the behavior is stored in the
geodatabase, not in map documents. A renderer can be forcibly linked in the geodatabase to a particular feature class
by implementing a feature class extension. See the section 'Managing custom feature renderers' later in this chapter
for more details.
You may want to create derived feature classes to symbolize your data. These feature classes may be the results of
geoprocessing the data to deconstruct shapes or generalize shapes to the extent that they can be easily handled and
drawn with the standard renderers. Effectively, you would be creating a cartographic database, where each base
feature class can have one or more derived feature classes. For datasets that are regularly edited, you could maintain
the derived features by implementing an editor extension, or feature class extension, which responds to edit events on
the base feature class by editing the derived features.
See Also Point Dispersal Renderer Example and Managing custom feature renderers.

Point Dispersal Renderer Example


Object Model Diagram

Example Code Click here.


Description This project enables a point feature layer to be drawn with the features moved so that none of their
symbols overlap. An accompanying property page allows the properties of the renderer to be set via the ArcMap user
interface.
Design PointDispersalRenderer is a subtype of the FeatureRenderer abstract class. PointDispersalPropertyPage
implements the standard property page interfaces.
License required ArcView or above.

200

Libraries Carto, CartoUI, Display, DisplayUI, Framework, Geodatabase, Geometry, and System
Languages Visual Basic
Categories ESRI Renderer Property Pages
Interfaces IFeatureRenderer, IPersistVariant, ILegendInfo, IComPropertyPage, IComEmbeddedPropertyPage, and
IRendererPropertyPage.
How to use
1.

Register PointDispersalVB.dll and double-click the PointDispersalVB.reg file to register to component


categories.

2.

Open ArcMap and add a few layers to the mapmake sure at least one layer contains point features. You
can use the 'dispersalrenderer_miscpoints' shapefile in the Samples/Data/ExtendingArcObjects folder of
the ArcGIS Developer Kit.

3.

Zoom the map until you have difficulty seeing the individual points as they overlap.

4.

Right-click the layer that has point features in the table of contents and click Properties. In the Layer
Properties dialog box, click the Symbology tab.

5.

Click the custom renderers category and click Point Dispersal Renderer.

6.

Select the properties you want for your renderer, and click OK to dismiss the dialog box.
Your layer will now draw using the point dispersal renderer.

The case for a point dispersal renderer


Imagine that you have a point feature class where many of the points lie close to or on top of each other. You would
like to force the points to all display individually, even if it means that the point is drawn slightly away from its true
location.

Some point datasets have features close to or an top of each other. A custom feature renderer can
be developed to disperse the point symbols so all the features can be seen.
Before describing the custom feature renderer solution, it is worth noting that there are many different approaches to
this problem. A custom layer is probably a more complete solution, since for this particular problem the custom
renderer will result in the selection tools not working properly.
Another approach to dispersing the points would be to label the features with a single character, placed directly over
the feature; the labelling functionality could then be used to avoid overlapping labels, although in this scenario it is
hard to ensure all features are labelled.
Creating a subtype of FeatureRenderer
By reviewing the Display object diagram, you can see that all renderer classes inherit from the FeatureRenderer
abstract class. Therefore, any type of custom renderer you create should begin by implementing the IFeatureRenderer
interface, along with interfaces for cloning and persistence.

201

You can see that there are a few other interfaces that are commonly implemented by a renderer, such as
IRendererFields, IRotationRenderer, IBarrierProperties2, IDataExclusion, IDataNormalization, ILookupSymbol, and
ITransparencyRenderer. You will not need to implement any of these interfaces in this example; however, you can find
more information on implementing these interfaces at the end of this example.
In the DisplayUI object diagram, you can also see that each renderer has an associated property page class. In the
ArcMap user interface, this not only allows a user to assign an instance of the custom renderer to a layer, but also to
alter the properties of the renderer.

Creating the PointDispersalRenderer

To answer the display requirements described above, you will create a custom feature renderer called
PointDispersalRenderer, that disperses the points as necessary to avoid their symbols overlapping.
You will also provide an accompanying property page implementation for your class.
Implementing IFeatureRenderer
The IFeatureRenderer interface is the core of a renderer. The main method that will be called by the ArcGIS framework
is Draw, at which point, it is the job of your renderer to draw the feature layer in any way you specify.
The Draw method receives a reference to the Display to which the renderer should draw and also a feature cursor
indicating the features to be drawn. Start by identifying the Symbol you will be drawing the feature with; this is stored
in the LegendGroup for this renderer (see the Implementing ILegendInfo section for more information).
[Visual Basic 6]

Private Sub IFeatureRenderer_Draw(ByVal Cursor As IFeatureCursor, _


ByVal drawPhase As esriDrawPhase, ByVal Display As IDisplay,
ByVal trackCancel As ITrackCancel)
...
Dim pSym As ISymbol
Set pSym = m_pLegendGroup.Class(0).Symbol
Display.SetSymbol pSym
...
Dim pFeature As IFeature
Set pFeature = Cursor.NextFeature
Draw also receives a trackCancel parameter, which indicates if the user has pressed the Esc key to cancel the drawing.
This is important, since the point dispersal could become slow in extreme situations with large datasets. This is a
suitable point to check the cancel tracker. You should also check this cancel tracker at the end of the main drawing
loop.
[Visual Basic 6]

Dim bContinue As Boolean


If Not trackCancel Is Nothing Then bContinue = trackCancel.Continue

202

The Draw method provides the main functionality of a renderer; Draw receives a reference to a
FeatureCursor, which contains all the features the renderer should draw.
You should check the cancel tracker and stop drawing if it indicates the user has pressed Esc.
Implement the main Draw method loop by iterating through the feature cursor, taking each feature in turn, and
drawing to the specified Display by calling the PlaceFeature function to find the dispersed location chosen for the
feature.
[Visual Basic 6]

Do While (Not pFeature Is Nothing) And (bContinue = True)


Dim pPoint As IPoint
Set pPoint = pFeature.Shape
...
PlaceFeature pPoint, 0, pGeomColl, Display, pSym, pPlacedPoint, _
pSymPoly, dDispersalDist
...
pGeomColl.AddGeometry pSymPoly.Envelope
Display.DrawPoint pPlacedPoint
...
Set pFeature = Cursor.NextFeature
If Not trackCancel Is Nothing Then bContinue = trackCancel.Continue
Loop
Each time a feature is drawn, add the Envelope of the feature to a GeometryBag variable, which holds the extent of all
the dispersed points placed so far and is passed in to the PlaceFeature function each time to allow the function to
identify where features have previously been placed.
Drawing the features
Note that the actual drawing is done with IDisplay::SetSymbol and then, for each feature, IDisplay::DrawPoint. This is
not typical for a custom renderer; it is general practice for the renderer to pass the relevant symbol to the
IFeatureDraw::Draw method on the feature. Calling IFeatureDraw::Draw allows custom features to use their own
drawing methods. In the case of the point dispersal example, the rendering is incompatible with IFeatureDraw since
the feature is to be drawn potentially away from its true location.
Generally, custom renderers should draw the features on the display by calling IFeatureDraw::Draw.
If drawing directly to the display, there is no need to call IDisplay::StartDrawing and IDisplay::FinishDrawing, since
you are already inside a drawing phase started by the ArcGIS framework.
PlaceFeature function
The actual dispersal of the points is done in the PlaceFeature function. A geometry bag of symbol envelopes is cached,
which records all the currently drawn points on the display.
Each feature is first placed at its true location. If the symbolized feature overlaps any of the already drawn features in
the layer, then a new attempt to place it is made at a certain distance away.

All four points of the compass are tried, and then the dispersal distance is increased until the feature is eventually
placed, and its envelope added to the geometry bag.
PlaceFeatures calculates a new location for a feature, so it does not overlap other features that have
already been drawn.
Layer draw phases

There are three draw phases for a layergeography, annotation, and selection. Except for the selection phase, the
Draw method of a renderer will be called for each phase that you specify in IFeatureRenderer::RenderPhase.
[Visual Basic 6]

203

Private Property Get IFeatureRenderer_RenderPhase(ByVal drawPhase As _


esriDrawPhase) As Boolean
If drawPhase = esriDPGeography Then
IFeatureRenderer_RenderPhase = True
Else
IFeatureRenderer_RenderPhase = False
End IfEnd Property
Draw is then also called for the selection phase if there are selected features on the display. The example chooses to
ignore this phase, since the features are dispersed from their true locations and thus incompatible with the selection
tool. Incidentally, raising E_FAIL from your Draw routine for a selection phase will result in the default selection
rendering for the layer.
For an example of a renderer that uses the annotation phase, see the 'BivariateRenderers' sample in the ArcGIS
Developer Help.
Drawing during debugging
There is one more thing to mention about drawing features to the display. If you are debugging your custom renderer
with Visual Basic 6, the features will not display on the map, as the Display has a process-dependent device context.
See Chapter 2, 'Developing Objects', for more information on debugging.
With the Visual Basic 6 debugger, the features drawn by a custom renderer will not appear on the
display.

204

Preparing the query filter


Before IFeatureRenderer::Draw is called, you are given an opportunity to modify the query filter that produces the
feature cursor in the PrepareFilter method. In this method, you must add into the filter any fields you need for your
renderer. The point dispersal renderer does not rely on any particular attributes.
The PrepareFilter method gives you a chance to specify which fields your renderer needs to perform
drawing.
However, the ExclusionSet property allows the framework to specify that a renderer should exclude a certain set of
features from drawing.
[Visual Basic 6]

Private Property Set IFeatureRenderer_ExclusionSet(ByVal pFeatureIDSet _


As IFeatureIDSet)
Set m_pExclusionSet = pFeatureIDSet
End Property
Therefore, if there is a set of features that have been specified in ExclusionSet, you need to make sure that the object
ID field name is added to the QueryFilter parameter.
[Visual Basic 6]

Private Sub IFeatureRenderer_PrepareFilter(ByVal fc As IFeatureClass, _


ByVal QueryFilter As IQueryFilter)
If Not m_pExclusionSet Is Nothing Then
If m_pExclusionSet.Count > 0 Then
QueryFilter.AddField fc.OIDFieldName
End If
End If
End Sub
If you implement IFeatureRenderer::ExclusionSet, you must ensure the object ID field is fetched
with PrepareFilter.
Although the PointDispersalRenderer example implements IFeatureRenderer::ExclusionSet, it is unlikely that you
would find that an exclusion set is used with this renderer in ArcMap, since the ExclusionSet is mainly related to the
Convert Features to Graphics command, which is inappropriate with this renderer.
Other client programs however may make their own exclusion sets. Note that if clients call
IFeatureRenderer::ExclusionSet directly it will be ignored, since the feature layer exclusion set overrides the renderer
exclusion set. If you use IGeoFeatureLayer::ExclusionSet this will be passed down to the renderer.
The query filter is used by the ArcGIS framework to produce the feature cursor passed to the Draw method. It is
actually a spatial filter (you could QI for ISpatialFilter to prove this to yourself), with the display extent being used to
limit which features are returned.
You will find that there are normally more features in the cursor than are within the display extent, since the spatial
filter criteria is set against the spatial index rather than the feature geometries. It is more efficient for the renderer to
draw these offscreen features than have a slower query. In the case of data that does not have a spatial index (for
example, some shapefiles), you will find all the features in the dataset are present in the feature cursor.
You may find features in the cursor that are not within the current display extent. It is generally
more efficient to draw these features than to check their extent yourself. ArcGIS has produced the
query for speed of execution.
For layers with feature class extensions or custom features, the query filter may already have some subfields set, as it
is the feature layer rather than the renderer that is responsible for checking
IFeatureClassDraw::RequiredFieldsForDraw.
CanRender property
If you want to restrict which layers your custom renderer can be applied to, such as being applicable only to line
layers, then in your implementation of IFeatureRenderer::CanRender, you can test properties of the feature layer and
return True if your renderer supports it and False if it does not.
The CanRender property should indicate if a renderer can draw a certain FeatureClass.
In your code, ensure the PointDispersalRenderer can only be applied to point layers.
[Visual Basic 6]

Private Function IFeatureRenderer_CanRender(ByVal featClass _


As IFeatureClass, ByVal Display As IDisplay) As Boolean
If featClass.ShapeType = esriGeometryPoint Then
IFeatureRenderer_CanRender = True
Else
IFeatureRenderer_CanRender = False
End If
End Function

205

Other types of renderers may check for other things about the FeatureClass or Display references passed infor
example, a renderer specially designed for networks may check if the FeatureClass contains a particular type of
network feature by checking the IFeatureClass::FeatureType property.
SymbolByFeature method
The SymbolByFeature method should return the symbol appropriate to a given feature. For the
PointDispersalRenderer, this is simple, since the point dispersal renderer only uses one symbol for all featuresnote
that you can only return the original locations of the features.
SymbolByFeature is called repeatedly by the ArcMap Convert Features to Graphics tool, and hence this command,
when called on a layer symbolized with a PointDispersalRenderer, will generate graphics in the original feature
locations.
SymbolByFeature should return the symbol the renderer would use to draw a specific, individual
feature.
Using SymbolByFeature also enables the possibility of containing other renderers within your custom renderer.
Imagine that in the example, you would like to disperse the points, but instead of a single symbol, you use one of
other symbology options such as proportional symbols or unique values. This could be achieved by keeping a reference
to a contained renderer class, your custom renderer, then for each feature in the Draw loop, calling SymbolByFeature
on the contained renderer to determine the symbol to use. In the ArcGIS Developer Help, you can see that the
'BivariateRenderers' sample custom renderer operates in this way.
Implementing ILegendInfo
ILegendInfo is often quite straightforward to implement. This interface ensures the table of contents and legends are
able to show a list of the symbols, labels, and headings your renderer is using. In the IFeatureRenderer::Draw
method, you have already seen how you can reuse the existing LegendGroup and LegendClass objects and use these
to hold the symbols with which your custom renderer will draw.
ILegendInfo helps link a renderer with the table of contents.
Declare a member variable to hold a reference to a LegendGroup.
[Visual Basic 6]

Private m_pLegendGroup As ILegendGroup


Use m_LegendGroup to return the values of the LegendGroupCount, LegendGroup, and LegendItem properties. The
LegendGroup is set up in the class initialization code and by the IDispersalRenderer interfacesee the example code
project for full details.
Return False from SymbolsAreGraduated, and do not allow this property to be changed, as you will not implement any
symbol graduation functionality to the PointDispersalRenderer.
Creating and implementing IDispersalRenderer
You need to provide a way for clients to change the Symbol used by the PointDispersalRenderer and also the dispersal
distance.
Create an interface called IDispersalRenderer, with two read-write properties, DispersalRatio and Symbol. Implement
IDispersalRenderer in the PointDispersalRenderer coclass. For more information on how you can create a new
interface, see Chapter 2, 'Developing Objects'.
[Visual Basic 6]

Private Property Set IDispersalRenderer_Symbol(ByVal pSymbol As ISymbol)


Set m_pLegendGroup.Class(0).Symbol = pSymbol
End Property
The custom IDispersalRenderer interface provides access to the symbol and dispersal distance used
by the renderer.
The Symbol property is set into the first Class of the LegendGroup, which means that the LegendGroup contains the
correct symbol and will display correctly in a legend or table of contents. Note that the Symbol property is passed by
reference.
[Visual Basic 6]

Private Property Let IDispersalRenderer_DispersalRatio(ByVal RHS As Double)


m_dDispersalRatio = RHS
End Property
The dispersal ratio value is used by the PlaceFeature function, as described previously. You will create a renderer
property page, which will be the main consumer of IDispersalRenderer.
Implementing persistence
You must implement the standard persistence interface or interfaces, to preserve the state of the renderer in a map
document (.mxd) or layer file (.lyr). Implement IPersistStream and IPersistStream if using VC++ or IPersistVariant if
using VB.
A renderer must be persistable.
In your PointDispersalRenderer, you need to save the legend group, which is the dispersal distance ratio. Any objects
you persist must implement IPersistStream (as does the legend group) or IPersistVariant.

206

[Visual Basic 6]

Private Sub IPersistVariant_Save(ByVal Stream As esriSystem.IVariantStream)


'Persistence version number
Stream.Write m_lCurrPersistVers
Stream.Write m_pLegendGroup
Stream.Write m_dDispersalRatio
End Sub
See Chapter 2, 'Developing Objects', for more information about the version checking used in the
PointDispersalRenderer persistence code.

Renderer property pages


Implementing a custom renderer property page will allow users to interact with the settings of your custom renderer.
By registering the property page in the ESRI Renderer Property Pages component category, the page will appear on
the Symbology tab of the Layer Properties dialog box along with all the standard symbology options. The Symbology
tab is itself a property page; therefore, your property page needs to be an embedded property page.

Define your custom renderer property page as a class called PointDispersalPropertyPage, that implements the
standard interfaces for an embedded property page and the IRendererPropertyPage interface.
Design your UI on a form as shown belowyou can place all the controls and descriptive text for the main part of the
page onto another control, which has a window handle (the example project uses a Picture box control). Reference this
form through a private data member in the PointDispersalPropertyPage class.

Implementing property page interfaces for the PointDispersalPropertyPage


The interfaces implemented on a property page class are dependent upon your development environment; refer to
Chapter 2, 'Developing Objects', for general information on implementing property page interfaces. This discussion will
follow the use of the interfaces implemented in the VB example project.
The Applies method may not actually be called for an embedded page; however, it is best practice to implement this
method fully anyway. Return True if you find a PointDispersalRenderer.
[Visual Basic 6]

Objects.Reset
Set pObj = Objects.Next
Do While Not TypeOf pObj Is IDispersalRenderer
Set pObj = Objects.Next
If pObj Is Nothing Then
IComPropertyPage_Applies = False
Exit Function
End If
Loop
In the SetObjects method, you are passed a set of objectsyou should find the renderer in this list, check it is a
PointDispersalRenderer, then initialize the controls on the accompanying form using the properties of the supplied
renderer.
[Visual Basic 6]

207

Dim pObj As Variant


Set pObj = Objects.Next
Do Until pObj Is Nothing
If TypeOf pObj Is IDispersalRenderer Then
Set m_pRenderer = pObj
m_frmPage.InitControls m_pRenderer
End If
Set pObj = Objects.Next
Loop
The Applies and SetObjects methods should both check that they are passed a reference to a
PointDispersalRenderer.
The PointDispersalRenderer property page only requires a reference to the renderer itself. However, the object set
passed to SetObjects will also include the map, feature layer, and feature class. If you are adapting this example to
create a different kind of renderer, you may need these references to allow users to set the properties of the renderer
correctly.
The Apply method is triggered when the user clicks Apply or OK on the layer properties property sheet. After calling
this method the framework will set the renderer supplied in SetObjects as the live renderer. As shown in the example
project code, you can use the implementation of IComEmbeddedPropertyPage::QueryObject to apply the changes to
the renderer object.
[Visual Basic 6]

Private Sub IComPropertyPage_Apply()


IComEmbeddedPropertyPage_QueryObject m_pRenderer
End Sub
Use IComPropertyPage::Priority to control where your renderer appears in the listbox of available renderers. Use a
lower number to have your renderer and category appear toward the top of the list (the priority of the first page in a
category controls where that category fits in the list). Generally, you should use a high number for custom renderers
to ensure they display after the standard renderersfor the PointDispersalPropertyPage, return a value of 600.
The table below lists standard renderer property pages and their priorities.
Type
Features

Categories

Charts

Attributes

Name

Priority

Single symbol

100

Unique values

200

Unique values, many fields

210

Match to symbols in a style

300

Graduated symbols

310

Proportional symbols

320

Dot density

330

Pies

400

Bars

410

Stacked

420

Quantity by category

500

Implementing IComEmbeddedPropertyPage
Custom renderer property pages fall into the class of embedded property pages. In VC++ you should ensure you
implement the CreateCompatibleObject and QueryObject members of IPropertyPageContext; in VB you will need to
implement IComEmbeddedPropertyPage.

208

In ArcMap, users choose from different symbology options from the tree view on the Layer Properties Symbology tab.
Because the internal representation of each option is a different renderer object, as the user chooses a new option, a
new renderer is being edited. In some cases, properties are preserved during this transition. For example, when a user
switches between the Bar chart and Pie chart options, the renderer fields and symbols are preserved from the old to
the new renderer.
As you will create an embedded property page, the properties of your renderer can be preserved
when users switch between types of renderer.
In addition to managing the retention of properties from an old renderer, you should also use CreateCompatibleObject
to avoid excessive cloning of renderers. In this method check to see if the in parameter is an object of the type your
page should edit. If so, return that same object. If not, create and return a new renderer object of the proper type.
[Visual Basic 6]

Private Function IComEmbeddedPropertyPage_CreateCompatibleObject(ByVal kind As Variant) As Variant


Dim pDispersalRend As IDispersalRenderer
If TypeOf kind Is IDispersalRenderer Then
Set pDispersalRend = kind
Else
Set pDispersalRend = New PointDispersalVB.Renderer
...
End If
Set IComEmbeddedPropertyPage_CreateCompatibleObject = pDispersalRend
End Sub
If you want, you can also copy any compatible properties you can find.
[Visual Basic 6]

...
Set pDispersalRend = New PointDispersalVB.Renderer
If TypeOf kind Is ILegendInfo Then
Dim pLegendInfo As ILegendInfo
Set pLegendInfo = kind
If pLegendInfo.LegendGroupCount > 0 Then
If TypeOf pLegendInfo.LegendGroup.Class(0).Symbol Is IMarkerSymbol Then
Set pDispersalRend.Symbol = pLegendInfo.LegendGroup.Class(0).Symbol
End If
End If
End If
...
CreateCompatibleObject should return a PointDispersalRenderer. You can attempt to copy any
compatible properties from the renderer reference passed in to the PointDispersalRenderer.
In the QueryObject method, apply the changes made on the property page to the supplied object. This renderer will
become the live renderer for the layer.
[Visual Basic 6]

Private Sub IComEmbeddedPropertyPage_QueryObject(ByVal theObject As Variant)


Dim pRenderer As IDispersalRenderer
If Not theObject Is Nothing Then

209

If (TypeOf theObject Is IDispersalRenderer) Then


Set pRenderer = theObject
m_frmPage.ApplyToRenderer pRenderer
End If
End If
End Sub
Typically, a property page creates a temporary object and allows changes to this object. Then, if the Apply or OK
buttons are clicked, the temporary renderer replaces the 'live' renderer object on the feature layer. If the Cancel
button is clicked, then the temporary renderer is discarded.
The ArcGIS framework automatically creates the temporary renderer by cloning the renderer on the layer before
passing it to your page, so it is not necessary for your code to make a copy. Note that this cloning will make use of the
persistence code for your renderer, as renderers do not support IClone.
Implementing IRendererPropertyPage
All renderer property pages implement an additional interface IRendererPropertyPage. Some of its properties will
appear on the ArcMap Symbology property page, which will be the container of your property page when displayed in
the Layer Properties dialog box. These properties help guide users when accessing your custom page.
The Description string will appear at the top of the parent page, and the PreviewImage will appear in the bottom left of
the dialog box. A preview image size of 116 by 88 pixels will display at a 1:1 ratioif the image is larger or smaller, it
will be scaled to fit the preview box. IRendererPropertyPage::Name appears in the tree view on the left side of the
symbology property page.
For IRendererPropertyPage::Type, use 'Custom Renderers', so that your renderer displays in the same category as the
other renderer developer samples. If you use an already existing Type (for example, 'Features'), your renderer will
appear under that category, listed by Priority order.

IRendererPropertyPage is used to edit the items on a property page that are common to all
renderers. The Preview property gives users an idea of how a renderer will display.
The Type property dictates where your renderer will appear in the listbox of available renderers. The Name property is
used to display an entry for the custom renderer in the list.
In IRendererPropertyPage::CanEdit you should check the in parameter to make sure your custom page can edit the
specified renderer. Typically, your custom property page will only edit your custom renderer. For the
PointDispersalPropertyPage, you can check for the presence of the IDispersalRenderer interface to identify your
renderer.
[Visual Basic 6]

Private Function IRendererPropertyPage_CanEdit(ByVal obj As _


IFeatureRenderer) As Boolean
If TypeOf obj Is IDispersalRenderer Then
IRendererPropertyPage_CanEdit = True
Else
IRendererPropertyPage_CanEdit = False
End If
End Function
If you create a different kind of renderer, it is well worthwhile defining an interface, which will uniquely identify your
renderer to help you implement CanEdit.
Note that standard renderers also implement CanEdit in this way, which has implications for the interfaces you might
like to implement on a custom renderer.

210

For example, it might make sense for the point dispersal renderer to implement ISimpleRenderer, since all of its
methods and properties are appropriate. However, this would cause problems with the property page. The
Features/Single Symbol property page will return True from CanEdit for any renderer that implements
ISimpleRenderer. As this page has a higher priority than the custom property page, the wrong page would be shown
for a point dispersal renderer. In practice, it is straightforward to avoid implementing the interfaces that identify the
standard renderers as they are all named similarly to their coclasses.
Each renderer has an interface that identifies it to its property page.
Now you are ready to use your renderer. See the Managing custom feature renderers topic for further advice.
See Also Customizing the Display, About custom feature renderers, and Managing custom feature renderers.

Managing Custom Feature Renderers


Custom feature renderers are quite simple to manage, generally just requiring the DLL containing the renderer class to
be registered on each client PC.
There are three main methods for applying a custom feature renderer to a layer:

From the Symbology tab of the Layer Properties dialog box

From client ArcObjects code

Via a feature class extension

Applying the renderer through the Layer Properties dialog box requires you to implement a custom renderer property
page registered to the 'ESRI Renderer Property Pages' component category. This is described previously for the
PointDispersalRenderer.
Applying your custom renderer with ArcObjects code is the usual method when you have not implemented a custom
renderer property page. The following VBA script creates a point dispersal renderer object (you will need to add a
reference in the VBA environment to the custom renderer's DLL), then replaces an existing renderer in a particular
layer.
[Visual Basic 6]

' pGeoFeatureLayer is an interface pointer to the IGeoFeatureLayer


' interface on a Feature Layer object.
' Create the custom renderer
Dim pMyRenderer as IDispersalRenderer
Set pMyRenderer = New PointDispersalVB.Renderer
' You could set some properties here
' Now set the custom renderer into the feature layer
Set pGeoFeatureLayer.Renderer = pMyRenderer
pMxDocument.ActiveView.Refresh
pMxDocument.UpdateContents
A custom renderer without an accompanying property page can be applied programmatically.
The third way of applying a custom renderer to a layer is by writing a feature class extension. Your class extension
must implement IFeatureClassExtension and IFeatureClassDraw.
In brief, the GUID of the FeatureClassExtension object is stored as an entry in the geodatabase. When the layer for
this feature class draws, it looks to the feature class extension and uses the renderer defined there
(IFeatureClassDraw::CustomRenderer), which can be either a custom renderer or one of the standard ESRI renderers.
You can also associate a custom renderer property page through
IFeatureClassDraw::CustomRendererPropertyPageCLSID. If you want to prevent the users from changing the
renderer, return True from IFeatureClassDraw::ExclusiveCustomRenderer.
Custom renderers can be applied by feature class extensions that implement IFeatureClassDraw.
For more information about writing feature class extensions, see Chapter 8, 'Customizing the geodatabase'. For an
example implementation of setting the default renderer with a class extension, see the 'FeatureClassDraw' sample in
the ArcGIS Developer Help.
See Also Point Dispersal Renderer Example and Custom Feature Renderers.

211

Chapter 6: Adapting the Catalog


The following sections provide examples of customizing the catalog object model.
GxObjects
About GxObjects and GxObjectFactories
Introduction to how GxObjects are used in the Catalog and the GxObject object model
GxInterchangeObject Example
An example of a GxObject, which allows you to work with Interchange (.E00) files in the Catalog. In this example you
can find the following customizations:

GxInterchangeObject

GxInterchangeFactory

Creating other kinds of GxObject and GxObjectFactory


Advice on creating other kinds of GxObjects and implementing other GxObject and GxObjectFactory interfaces.
GxFilter Interchange Files Example
An example of a GxFilter object, which can be used to browse for Interchange files in the GxDialog
See Also
Simple Point Layer Example
The simple point layer example includes as part of the customization a GxObject to view the specific data format in the
Catalog.

Adapting the Catalog


The following sections provide examples of customizing the catalog object model.
GxObjects
About GxObjects and GxObjectFactories
Introduction to how GxObjects are used in the Catalog and the GxObject object model
GxInterchangeObject Example
An example of a GxObject, which allows you to work with Interchange (.E00) files in the Catalog. In this example you
can find the following customizations:

GxInterchangeObject

GxInterchangeFactory

Creating other kinds of GxObject and GxObjectFactory


Advice on creating other kinds of GxObjects and implementing other GxObject and GxObjectFactory interfaces.
GxFilter Interchange Files Example
An example of a GxFilter object, which can be used to browse for Interchange files in the GxDialog
See Also
Simple Point Layer Example
The simple point layer example includes as part of the customization a GxObject to view the specific data format in the
Catalog.

About GxObjects and GxObjectFactories


GxObjects in ArcCatalog
Items in the catalog representing disk connections, files, disk connections, datasets, and so on, are represented
programmatically by GxObjects. Each item showing the location of a file, folder, dataset, and so on, is represented
internally by a separate GxObject object. Different types of data are represented by different classes of GxObjectfor
example, a layer file is represented by an instance of GxLayer and a map document by a GxMap.

212

In ArcCatalog, GxObjects are used to represent geographic datasets, datafiles, folders, database
connections, and other forms of data.
Looking at the Catalog and CatalogUI object model diagrams, you will see a top-level abstract class, GxObject.
Inheriting from this are a coclass, GxFile, and an abstract class, GxObjectContainer. All other GxObject coclasses
inherit from either GxFile or GxObjectContainer. This division illustrates the differences between GxObjects that
represent folder-based data (GxObjectContainers) and file-based data (GxFiles).

GxFiles represent file-based data; GxContainers represent a data source, such as a folder full of
shapefiles, or a disk connection. Both GxFile and GxContainer are types of GxObject.
There are existing GxObjects available to represent most of the common data types; however, there is no GxObject to
represent ArcInfo Interchange files, so you cannot view interchange files in the Catalog.
ArcCatalog can only show data that has a corresponding GxObject.
The object model diagram also shows that each GxObject is instantiated by a GxObjectFactory. There is a
corresponding factory class for each of the individual object classesfor example, GxLayer objects are instantiated by
the GxLayerFactory.

How GxObjects and GxObjectFactories are used


When ArcCatalog starts, an instance of each GxObjectFactory is created. When a user clicks on a folder in the tree
view or double-clicks a folder in the contents view, ArcCatalog needs to display the contents of that folder.
First, ArcCatalog checks with each available GxObjectFactory to see if the folder contains any data of each type. If the
data is present, the factory object is then asked to instantiate a GxObject to encapsulate each item of that data type.
When the GxObjects are returned to ArcCatalog from the factory object, they are linked to their parent GxObject by
ArcCatalog using the Attach method. The Attach and Detach methods are used to connect a GxObject to the parent
application by weak references, avoiding the creation of circular references.

213

Each GxObject has an associated GxObjectFactory. GxObjectFactories are used to identify the
presence of a particular type of data and to create an appropriate GxObject to represent the data in
ArcCatalog.
ArcCatalog uses the data encapsulated in GxObjects when a user performs a drag-and-drop, or copy-paste operation.
The GxObject may also be asked for relevant metadata.
Creating a GxObject adds both data and behavior to ArcCatalog. Information is added by the
identification of the new data type, and behavior is extended by the ability to drag and drop and
copy and paste the new data type.

GxObject Metadata
ArcObjects provides the ability to store metadata with each GxObject. Metadata files are as XML files whose elements
contain information about the GxObject. Some information needs to be completed manually (for example, a
description of the data and its purpose), and some can be completed automatically (for example, the size and location
of a dataset). To write this information, each GxObject makes use of MetadataSynchronizers, which are objects that
help write standardized information to the metadata file. An XMLPropertySet object is used to represent the contents
of a metadata file.

Metadata is optional functionality, which may not be appropriate for all GxObjectsfor example, the GxNewDatabase
class does not implement metadata interfaces.
Metadata standards
Metadata standards determine what information is written to a metadata file, what the structure of elements is, and
how the information is formatted in those elements.
By default, metadata created by ArcGIS complies with version 2 of the Federal Geographic Data Committee's (FGDC)
Content Standard for Digital Geospatial Metadata (CSDGM), the details of which can be found on the FGDC Web site at
www.fgdc.gov. ESRI has extended this standard, resulting in the ESRI Profile of the CSDGM, details of which can be
found on the ESRI Web site at www.esri.com/metadata/esriprof80.html.
In the case of the GxInterchangeObject, you may not have access to sufficient information about the underlying
interchange file to complete metadata to this standard. Therefore, this example will demonstrate how you can tailor
the use of the metadata synchronizers to include only the metadata attributes you require. Note that some of the
metadata objects you will use when implementing metadata for the GxInterchangeObject are designed to work with
specific standards (for example, the FGDCSynchronizationHelper). However, the metadata produced by the
GxInterchangeObject does not completely fulfill the FGDC standard and, therefore, will not indicate in the metadata
any specific standard.
You could avoid the use of these standard-related objects and produce metadata that complies solely with a different
metadata standard, in which case you may want to investigate the 'Creating a Custom Metadata Synchronizer' white
paper, which is available via ArcObjects Online.
See Also Interchange GxObject Example, Creating other kinds of GxObject and GxObjectFactory, and Interchange
GxFilter Example.

214

GxInterchangeObject and GxInterchangeFactory Example


GxInterchangeObject Example
Object Model Diagram

Example Code Click here.


Description This example provides a GxObject, which can be used to identify ArcInfo Interchange (.e00) files in
ArcCatalog. A factory coclass allows ArcCatalog to create custom GxObjects when they are required. With this
customization, Interchange files can be renamed, deleted and copied. Using the context menu, the Interchange file
can also be imported to a coverage. Note: this functionality will only be available for ArcInfo licenses; all other
functionality requires only an ArcView license.
Design GxInterchangeObject class inherits from GxObject, and GxInterchangeFactory inherits from GxObjectFactory.
License ArcView or above.
Libraries Catalog, CatalogUI, Framework, Geodatabase, System, and SystemUI
Languages Visual Basic
Categories ESRI GX Object Factories
Interfaces IGxObject, IGxObjectEdit, IGxObjectProperties, IGxObjectUI, IMetadata, IMetadataEdit, IGxObjectFactory,
and ICommand
How to use
1.

Register the GxInterchangeVB.dll, and double-click the GxInterchangeVB.reg file to register to component
categories.

2.

Open ArcCatalog.

3.

Browse to an ArcInfo Interchange (.E00) file.


As well as being able to see the Interchange file, you can now right-click the file to display the context
menu, including the import option. (Note: To run the import tool you will need an ArcInfo license.)

215

The case for a GxInterchangeObject


You can browse data files using Windows Explorer. However, this is not the ideal tool for browsing geographic data, as
it will display a multiple-file dataset as individual files and cannot display nonfile data sources such as personal or
enterprise geodatabases.

A major part of ArcCatalog functionality is to allow you to browse your datafiles in a data-centric manner. ArcCatalog
presents a view of your data, which contains some understanding of geographical data formats.
However, not every type of geographical data format is recognized as such by ArcCatalog. ArcInfo Interchange format
is a file format, which you may have used for the transfer of ArcInfo coverages. Interchange files have the extension
.e00, and contain ASCII data, which can be opened and viewed using any text editor.

By using the ArcView 8.x tools, interchange files can be imported to a coverage if you have an ArcInfo license.

216

By default, you will not be able to see ArcInfo Interchange files when browsing in ArcCatalog. If you commonly work
with interchange files, it would be useful to be able to browse to these files in ArcCatalog using easily identifiable
icons; create and edit metadata for the files; and perform simple file-based operations such as move, rename, and
delete, from within ArcCatalog.
It is possible to view files with any extension in ArcCatalog by adding new settings to the File Types tab in the
ArcCatalog Options dialog box. However, this would restrict the appearance and functionality of the files to the generic
behavior provided by ArcCatalog, which would not allow custom icons, context menus, metadata, and so on.
You can ask ArcCatalog to show you ArcInfo Interchange files by using the File Types tab in the
Options dialog box, but this will not allow you to customize the context menu or properties of files
viewed in this way.
The requirements for this customization are to view interchange files in the ArcCatalog tree view and to be able to
manipulate interchange files from within ArcCatalog by use of a context menu. You may also want to be able to work
with interchange files in the GxDialog; see the following GxFilterInterchange example.

Creating a subtype of GxObject

From the GxObject abstract class you can see that the IGxObject, IGxObjectUI, and IGxObjectEdit interfaces are
common to all types of GxObject. There are also a number of additional interfaces often found on GxObjects.
A simple, lightweight GxObject may be created by implementing the few basic interfaces; however, a GxObject may
become a relatively large and complex customization as the options for additional interfaces and functionality are
extensive. In more complex cases, care must be taken to avoid changing the default behavior of ArcCatalog.

Creating the GxInterchangeObject

You will create a component to view and manipulate interchange files in ArcCatalog in two main steps.
First, create a file-based GxObject class called GxInterchangeObject to represent the ArcInfo Interchange file data
format. You will implement the basic GxObject interfaces IGxObject, IGxObjectUI, IGxObjectProperties, and
IGxObjectEdit. To allow metadata to be created and edited, you will also implement the optional interfaces IMetadata
and IMetadataEdit.
Then create a GxObjectFactory called GxInterchangeFactory that can create the new GxObject.
These two objects must both be registered to create a usable customization. In addition to this, you can continue by
creating the GxFilterInterchange example in the following section.
In this example you will create a GxInterchangeObject class, which represents Interchange (.e00)
files and an accompanying GxInterchangeFactory.
Investigating files and folders
The GxInterchangeObject will need to perform some investigation of the file system, to show filenames and paths. The
ESRI object libraries do not provide objects for file system investigation; therefore, the VB example code uses the
FileSystemObject, which is part of the Microsoft Scripting Runtime library, scrrun.dll.

217

Implementing IGxObject
From the interface listing on the left, you can see that the majority of the properties on the IGxObject interface are
used for identification of the data.
IGxObject provides information about a GxObject to its clients.
The FullName property should include the full path and filename of the file, including extension. Name should return
the full filename, and BaseName should return only the filename without the extension.
ArcCatalog uses GxObjectFactories to create GxObjects that represent each file or dataset. The factory gives each
GxObject its Name before attaching the GxObject to the catalog tree. To allow the Name to be set, add a property,
which is internal to the project. In VB this is achieved by using a Friend property. You can then derive the values of all
the other read-only identification properties from this Name value. (See the later section 'Creating the
GxInterchangeFactory' for more information about how this property is used.)
[Visual Basic 6]

Friend Property Let Name(sName As String)


If sName <> "" Then
If Not (m_pFileSystemObject Is Nothing) Then
If (UCase(m_pFileSystemObject.GetExtensionName(sName)) = "E00") Then
m_sFullName = sName
m_sName = m_pFileSystemObject.GetFileName(sName)
m_sBaseName = m_pFileSystemObject.GetBaseName(sName)
End If
End If
End If
End Property
The GxInterchangeObject is set up via the writable Name property, which is visible to the
accompanying factory, but not to external classes.
The code shown includes an extra error checking step that checks the extension of the filename is ".E00". For
efficiency, the FileSystemObject is instantiated once, when the GxInterchangeObject itself is instantiated. The code
shown does not check for the presence of the named file on disk, although if required, you could adapt the code to use
the FileSystemObject's GetFile method, which does perform this check.
Once a GxObject is attached to the Catalog, the object will be cached and reused if its container is viewed again. A
GxObject will be re-created, however, if the Catalog view is refreshed; this happens if the user presses F5 or chooses
the Refresh command from the View menu. The GxInterchangeObject does not need to do anything for the Refresh
method; when the Parent folder is refreshed, it will drop and re-create its Children. However, if you are creating a
different type of GxObject (particularly a GxObjectContainer), you should ensure you release and re-create any
internal state in the Refresh method.
The Category property is unrelated to component categories. Return a string giving information about the GxObject;
ArcCatalog will use this to display the Type column information in the Contents view.
[Visual Basic 6]

Private Property Get IGxObject_Category() As String


IGxObject_Category = "Interchange (.e00) File"
End Property

ArcCatalog uses lightweight name objects to allow copy and paste functionality; by implementing InternalObjectName,
you add copy and paste functionality to your GxObject. This is straightforward to implement for this
GxInterchangeObject as you can simply return an appropriate FileName object from the property.
[Visual Basic 6]

Private Property Get IGxObject_InternalObjectName() As esriSystem.IName


Dim pName As IFileName
Set pName = New FileName
pName.Path = m_sFullName
Set IGxObject_InternalObjectName = pName

218

End Property
If you are creating another type of GxObject, it may be more appropriate to return a different type of IName object.
For example, the GxObject that represents a personal geodatabase is GxDatabase, which returns a WorkspaceName
object as the InternalObjectName property. If the FileName object is unsuitable for your data type, there are
numerous alternative IName objects you could use instead.
The Category property is displayed in ArcCatalog and is unrelated to component categories.
InternalObjectName adds copy-and-paste functionality.
Apart from identification, the IGxObject interface is also used by clients to keep track of the object's position within the
ArcCatalog tree view via the Parent and Attach members. You should cache both the references, which are passed in
to the GxInterchangeObject's Attach method:

Parent is a reference to the parent item of the GxInterchangeObject, a GxFolder.

pCatalog is a reference to the GxCatalog of the ArcCatalog application.

[Visual Basic 6]

Private Sub IGxObject_Attach(ByVal Parent As esriCatalog.IGxObject, _


ByVal pCatalog As esriCatalog.IGxCatalog)
Set m_pParent = Parent
Set m_pCatalog = pCatalog
End Sub
Using the references passed to Attach, a GxObject can find out more about its location if required.
For example, a GxDataset uses its Parent to work out if the GxDataset resides in an enterprise or
personal geodatabase.
You should release these cached references in the Detach method, so that they are cleaned up explicitly before the
GxObject is terminated. The Attach and Detach methods will be called by ArcCatalog when appropriate.
[Visual Basic 6]

Private Sub IGxObject_Detach()


Set m_pParent = Nothing
Set m_pCatalog = Nothing
End Sub
IsValid and GxObject validity
A GxObject must return True from the IsValid property to ensure the instance is valid for use in ArcCatalog;
ArcCatalog calls IsValid periodically, typically to ensure the GxObject is in a valid state prior to performing an
operation with it. As a minimum, your GxObject could check that it has references to a valid Parent and Catalog (from
the Attach method) before indicating the instance is valid.
[Visual Basic 6]

Private Property Get IGxObject_IsValid() As Boolean


If Not (m_pParent Is Nothing) Then
If Not (m_pCatalog Is Nothing) Then
IGxObject_IsValid = True
End If
End If
End Property
For the GxInterchangeFile, as long as the file extension is correct and there are valid Parent and Catalog references,
you will assume the GxObject represents a valid Interchange file.
If you are adapting this example, you may want to add further complexity to the IsValid property. If there is a problem
either parsing the incoming filename or with the file's contents, you may want to indicate that the GxObject is invalid
by returning False from the IsValid property. For example, most file-based GxObjects check to see if the file exists on
disk at the moment when the IsValid property is called. GxDataset goes further and checks to see if the dataset can be
opened by using the InternalNameObject (IName::Open) before returning the IsValid property.
You may want to use an alternate icon when IsValid is false to highlight the invalid data to the user. If you have added
an ArcIMS Server to ArcCatalog, a broken connection icon will be displayed for a broken connection to the Server. See
the 'Implementing IGxObjectUI' section below for more information on specifying the icons for a GxObject.
Implementing IGxObject by itself allows a class to be identified and used as a GxObject, but does not provide much
functionality. Therefore, you will now implement a number of other interfaces on your GxInterchangeObject.
If a GxObject is not valid, it is sometimes displayed with a different icon; broken connections to an
ArcIMS server are shown with a small red cross over the standard icon.

219

Implementing IGxObjectUI
The next interface you will implement on the GxInterchangeObject is IGxObjectUI. This interface is not mandatory for
a GxObject; it is possible to have a functioning GxObject that does not implement IGxObjectUI, but you will implement
it to provide custom functionality on the context menu of the GxIntercahngeObject.
IGxObjectUI provides a GxObject with icons and context-sensitive menus. You only need to create
the context-sensitive menu once, when the property is first called.
IGxObjectUI allows you to assign icons, which ArcCatalog will use for the display of GxInterchangeObjects in the tree
view. It also adds the significant functionality of allowing you to build your own context-sensitive menu, allowing you
to define the options available in ArcCatalog when a user right-clicks on a GxInterchangeObject.
To implement the ContextMenu property, you will need to create a context menu, a CommandBar, containing the
required commands.
[Visual Basic 6]

Private m_pCtxMenu As esriFramework.ICommandBar


For efficiency you should only create the CommandBar once when ContextMenu is first called. First, gain access to the
CommandBar collection of the current application using the AppRef object (see Chapter 2, 'Developing Objects' for
more information about using the AppRef object in a component).
[Visual Basic 6]

Private Property Get IGxObjectUI_ContextMenu() As esriSystem.IUID


If m_pCtxMenu Is Nothing Then
...
Dim pCmdBars As esriFramework.ICommandBars
Set pCmdBars = pApp.Document.CommandBars
Next, use the Create method of this CommandBars collection to create the new GxInterchangeObject's context menu.
[Visual Basic 6]

Set m_pCtxMenu = pCmdBars.Create("InterchangeMenu", _


esriSystemUI.esriCmdBarType.esriCmdBarTypeShortcutMenu)
Then add copy, paste, and delete commands to the context menu.
[Visual Basic 6]

Dim pUid

As esriSystem.IUID

Set pUid = New esriSystem.UID


pUid.Value = "{C637B93D-0FA5-11D3-9F4F-00C04F6BC69E}"

' CopyMenuItem

Dim pCmdItem As esriFramework.ICommandItem


Set pCmdItem = m_pCtxMenu.Add(pUid)
pUid.Value = "{25C0E6C1-CD06-11D2-9F40-00C04F6BC626}"

' DeleteMenuItem

Set pCmdItem = m_pCtxMenu.Add(pUid)


pUid.Value = "{25C0E6C3-CD06-11D2-9F40-00C04F6BC626}" ' RenameMenuItem
Set pCmdItem = m_pCtxMenu.Add(pUid)
You can use the existing commands for copy, delete, and rename for a custom GxObject's context
menu.
By using the ArcView 8.x tools in ArcCatalog, an interchange file can be imported to a coverage workspace. Add this
command to the context menu.
[Visual Basic 6]

pUID.Value = "{27CD46E9-2C2F-11D4-80FD-00C04F602966}"
Set pCmdItem = m_pCtxMenu.Add(pUID)
pCmdItem.Group = True
The last command on an ArcCatalog context menu should generally be the Properties command.
[Visual Basic 6]

pUid.Value = "{20724105-BAB8-11D1-9ABA-080009EC734B}" ' PropertiesMenuItem


Set pCmdItem = m_pCtxMenu.Add(pUid)

220

pCmdItem.Group = True
Generally, GxObjects have a Properties option as the last option on the context-sensitive menu. If
the user chooses this option, ArcCatalog will use the EditProperties method of IGxPropertiesEdit to
respond to the choice.
The commands chosen replicate the options that are found on the standard context menu for similar existing
GxObjects. A full list of the generic ArcCatalog menu IDs can be found in the Technical Documents section of the
ArcGIS Developer Help, under 'Names and IDs'.
Now that the menu is created, you can complete ContextMenu by calling the menu's Popup method to display the
command bar; return the UID of the command bar from the property.
[Visual Basic 6]

m_pCtxMenu.Popup
Dim pSelected_CmdItem As ICommandItem
Set pSelected_CmdItem = m_pCtxMenu
Set IGxObjectUI_ContextMenu = pSelected_CmdItem.ID

You do not need to implement the NewMenu property for your GxInterchangeObject. Users may instead generate
interchange files using the geoprocessing functionality in ArcGIS or by using ArcInfo Workstation.
The remaining members of IGxObjectUI are used to specify the icons which should be displayed in ArcCatalog. You can
implement these in the same way as you would the ICommand::Bitmap property, with which you should be familiar.
Two bitmaps are used to return the large and small icons.
[Visual Basic 6]

Private m_pBitmapSmall As IPictureDisp


Private m_pBitmapLarge As IPictureDisp
The small icon is used in the tree view and also in the list and details contents views. The large icon
is used by the Large Icons and Thumbnails contents views.
In the sample project code, LargeImage and LargeSelectedImage use the same bitmap (stored in a resource file), as
does SmallImage and SmallSelectedImage properties. You could, however, return a different image when your
GxObject is selected, if you want.

Implementing IGxObjectEdit
IGxObjectEdit adds file manipulation functionality to a GxObject. Although the interface does not perform editing of
the actual file contents, it does allow copy, rename, and delete operations to occur.
IGxObjectEdit determines whether a GxObject can be copied, moved, deleted, and renamed and also
performs these operations.
You can prevent the rename or delete of a read-only interchange file by adding a function, CheckAttributes, which will
use the FileSystemObject to check if the Interchange file is read-only. In addition, the function can check if the file still
exists (it may have been removed by another program). Store the outcome of the two checks in member variables.
[Visual Basic 6]

Private m_bExists As Boolean


Private m_bReadOnly As Boolean
...
Private Sub CheckAttributes()
If Not m_pFileSystemObject Is Nothing Then
m_bExists = m_pFileSystemObject.FileExists(m_sFullName)
If m_bExists Then
Dim file As Object
Set file = m_pFileSystemObject.GetFile(m_sFullName)
If file.Attributes And 1 Then

221

m_bReadOnly = True
Else
m_bReadOnly = False
End If
End If
End If
End Sub
Using the member variables, return true from CanCopy if the file exists; CanRename and CanDelete should return true
if the file exists and is also not read-only.
[Visual Basic 6]

Private Function IGxObjectEdit_CanRename() As Boolean


CheckAttributes
IGxObjectEdit_CanRename = m_bExists And Not (m_bReadOnly)
End Function
The return values of CanCopy, CanDelete, and CanRename will determine if the context menu items
you added previously are enabled or disabled.
The copy, delete, and rename commands you added to the context menu will be enabled or disabled by ArcCatalog,
based on the values you return from CanCopy, CanDelete, and CanRename.
[Visual Basic 6]

Private Sub IGxObjectEdit_Delete()


CheckAttributes
If m_bExists And Not m_bReadOnly Then
If Not m_pFileSystemObject Is Nothing Then
m_pFileSystemObject.DeleteFile (m_sFullName)
Dim pContainer As esriCatalog.IGxObjectContainer
Set pContainer = m_pParent
pContainer.DeleteChild Me
End If
End If
End Sub
The Delete method should remove the underlying files (or other items) upon which a GxObject is
based. Delete should also ensure the current instance of the class is detached from the Catalog by
calling the class's own IGxObject::DeleteChild method.
In the Rename method, first check that the new name has the correct file extension. Append the "e00" extension if not
present, then rename the file by copying the file to the new name and deleting the old file.
[Visual Basic 6]

Dim sTemp As String


If InStr(1, UCase(newShortName), "E00") = (Len(newShortName) - 2) Then
sTemp = m_pParent.FullName & "\" & newShortName
Else
sTemp = m_pParent.FullName & "\" & newShortName & ".e00"
End If
m_pFileSystemObject.Copyfile m_sFullName, sTemp
m_pFileSystemObject.DeleteFile (m_sFullName)
Last, you must reset the member variables to reflect the name change. Call the class's Name method to ensure the
GxInterchangeObject is pointing at the renamed data correctly.
[Visual Basic 6]

Me.Name = sTemp
For the EditProperties method, you can use the generic file properties page to display the basic properties of the file.
[Visual Basic 6]

Private Sub IGxObjectEdit_EditProperties(ByVal hParent As esriSystem.OLE_HANDLE)


Dim pGxFile As IGxFile, pGxObjEd As IGxObjectEdit
Set pGxFile = New GxFile
pGxFile.Path = m_sFullName
Set pGxObjEd = pGxFile
pGxObjEd.EditProperties hParent
End Sub
The EditProperties method could alternatively be used to access your own custom functionality; see
the Creating other kinds of GxObjects section for more information.

222

Add a call to CheckAttributes just before displaying the context menu, ensuring the options are up-to-date.
Implementing IGxObjectProperties
This optional interface is not relied on for any major functionality. However, it does allow access to properties of a
GxObject without requiring a separate property or method to be declared to access each separate piece of information.
This allows new versions of your GxObject to gain additional functionality without breaking binary compatibility.
IGxObjectProperties allows access to properties of a GxObject in a flexible manner.
For the GetProperty method, you should support a minimum of two properties, name and type, which are accessed via
the strings ESRI_GxObject_Name and ESRI_GxObject_Type.
[Visual Basic 6]

Private Function
IGxObjectProperties_GetProperty(ByVal Name As String) As Variant
Select Case Name
Case "ESRI_GxObject_Name"
IGxObjectProperties_GetProperty = IGxObject_Name
Case "ESRI_GxObject_Type"
IGxObjectProperties_GetProperty = IGxObject_Category
As a file-based GxObject, the GxInterchangeObject can support FileSize, FileTime and FileMode, as shown below. Raise
an error if an unrecognized property is requested.
[Visual Basic 6]

Dim file As Object


Set file = m_pFileSystemObject.GetFile(m_sFullName)
Select Case Name
Case "ESRI_GxObject_FileSize"
IGxObjectProperties_GetProperty = Format(file.Size / 1048576, "0.000")
Case "ESRI_GxObject_FileTime"
IGxObjectProperties_GetProperty = file.DateLastModified
Case "ESRI_GxObject_FileMode"
IGxObjectProperties_GetProperty = "R/W"
If file.Attributes And 1 Then IGxObjectProperties_GetProperty = "R"
Case Else
Err.Raise E_INVALIDARG
End Select
From SetProperty you can return the E_FAIL error codelike most GxObjects, the GxInterchangeObject does not have
any writable properties (for example, clients should not be able to set the Name of a GxObject). To complete the
GetPropByIndex method, you can forward calls to the GetProperty method.
[Visual Basic 6]

Private Sub IGxObjectProperties_GetPropByIndex(_


ByVal index As Long, pName As String, pValue As Variant)
Select Case index
Case 0
pName = "ESRI_GxObject_Name"
...
Case Else
Err.Raise E_INVALIDARG
Exit Sub
End Select
pValue = IGxObjectProperties_GetProperty(pName)
End Sub

Adding Metadata Support


Before implementing the metadata interfaces for your GxObject, it is advisable to become familiar with the metadata
objects and the process of metadata creation and synchronization used by ArcObjects. You can find out more
information in the GxObject Metadata topic and by reading the ArcGIS Developer Help.
Implementing IMetadata
The IMetadata interface allows clients to create, edit, and view metadata for a GxObject. From Metadata, return the
XMLPropertySet, which contains metadata for the GxInterchangeObject. If metadata already exists, you can use a
GxMetadataFactory to return the XMLPropertySet of the metadata; if not, return a new, empty XmlPropertySet.
[Visual Basic 6]

Private Property Get IMetadata_Metadata() As esriSystem.IPropertySet

223

Dim pProp As esriSystem.IPropertySet


If ExistsMetadata(m_sFullName & ".xml") Then
Dim pGxObjectFactory As esriCatalogUI.IGxObjectFactoryMetadata
Set pGxObjectFactory = New esriCatalogUI.GxMetadataFactory
Dim pGxObject As esriCatalogUI.IGxObject
Set pGxObject = pGxObjectFactory.GetGxObjectFromMetadata( _
m_sFullName & ".xml")
Dim pMetadata As esriGeoDatabase.IMetadata
Set pMetadata = pGxObject
Set pProp = pMetadata.Metadata
Else
Set pProp = New esriGeoDatabase.XmlPropertySet
End If
Set IMetadata_Metadata = pProp
End Property
To allow the property to be set, replace the existing Metadata with the IPropertySet reference passed in; ensure you
check if the metadata file already exists and, if not, create the file.
By implementing IMetadata, a GxObject allows users to create, edit, and view metadata for that
object in ArcCatalog.
The synchronization process
The Synchronize method will be called by clients both to update metadata after changes have been made to a
GxObject and to generate a new set of metadata when it doesn't already exist.
The client determines which action is required, informing the GxObject by passing in the Action parameter to
Synchronize, for a GxObject in ArcCatalog, for example, the Action reflects the current settings in the Metadata tab of
the ArcCatalog Options dialog box.

The ArcCatalog Options dialog box allows a user to determine how metadata is edited and displayed
and when it is automatically created and updated.

224

The GxObject, in turn, decides what the metadata should contain and passes these new values to the
MetadataSynchronizer using the Update method of the IMetadataSynchronizer interface.

MetadataSynchronizer is a singleton that manages references to all current metadata synchronizers. In its Update
method, it will check the XMLPropertySet passed in to see if the named element can be updated (which is dependent
upon that element's Sync attribute). If the element can be updated, the manager will pass the XMLPropertySet and
the new value to all the currently enabled metadata synchronizers. Each synchronizer in turn will have the opportunity
to update the element in the XMLPropertySet.
Beginning Synchronization
In the Synchronize method, you therefore need to begin by retrieving the existing Metadata (by using the class's own
Metadata property, metadata will automatically be created if it does not already exist).
[Visual Basic 6]

Dim pXMLPropertySet As IXmlPropertySet2


Set pXMLPropertySet = IMetadata_Metadata
Next, create an FGDCSynchronizationHelper (you will not rely on this object to perform the synchronization entirely, as
discussed previously, the GxInterchangeObject will not be synchronized exactly to the FGDC standard).
Use the StartSynchronization method of ISynchronizationHelper to decide if it is appropriate to perform
synchronization at this point. The helper object will determine if synchronization is appropriate depending upon the
time Interval, Action, and the current values of the synchronization properties in the metadata.
[Visual Basic 6]

Dim pSynchronizationHelper As ISynchronizationHelper


Set pSynchronizationHelper = New FGDCSynchronizationHelper
Dim bSynchronize As Boolean
pSynchronizationHelper.StartSynchronization pPropertySet, Action, _
Interval, bSynchronize
If Not bSynchronize Then
Err.Raise S_FALSE
Else
...
The Creating and Updating metadata options in the ArcCatalog Options dialog box help determine
the value of the Action parameter passed to the Synchronize method of the metadata synchronizers.
Note that if the bSynchronize parameter returned from StartSynchronization is False, you should raise the S_FALSE
error back to GxObject's client before exiting the Synchronize method. This indicates that metadata is not available
and will ensure the behavior is the same as other existing GxObjects.
If you do not want to use an FGDCSynchronizationHelper object, you could alternatively determine yourself if
synchronization is appropriate by checking the Interval, Action, and the Esri\Sync element of the current metadata.
Using the FGDCSynchronizationHelper simply shortcuts the logic required here.
Writing metadata information
Now you can gather the information about the GxObject, which you will use to update the metadata, using your
knowledge about the GxObject, and also using Windows API calls. In the example code, the DatasetName,
DatasetLocation, NativeForm, Environment, OperatingSystem, Language, and MetadataDate metadata elements are
updated (full details of how the relevant information is gathered can be found in the sample project code, as it is more
a matter of general programming and not of GxObjects and their metadata).
[Visual Basic 6]

Dim vDataSetName As Variant, vNativeForm As Variant


vDataSetName = IGxObject_Name
vNativeForm = IGxObject_Category
...
Now that you have the information required, you can begin updating the metadata. First, create a
MetadataSynchronizer, then pass the new information in turn to this object using the IMetadataSynchronizer::Update
method, telling the synchronizer which metadata element the information should be written to.
[Visual Basic 6]

Dim pMetadataSynchronizer As IMetadataSynchronizer

225

Set pMetadataSynchronizer = New MetadataSynchronizer


pMetadataSynchronizer.Update pXMLPropertySet, "DatasetName", vDataSetName
pMetadataSynchronizer.Update pXMLPropertySet, "NativeForm", vNativeForm
...
The MetadataSynchronizer will determine if the individual element (for example, DatasetName) should be
synchronized, and if so pass the new value (for example, vDataSetName) to each metadata synchronizer, which will
then write this new value to the metadata file (for example, pXMLPropertySet).
Remember that every metadata standard will require a particular set of information to be completed, and it is unlikely
that your GxObject will be able to complete all the information automatically during synchronization.
Completing synchronization
If IXmlPropertySet::IsNew is True, you should set the SyncOnce element to False at the end of your Synchronize
method to indicate that the metadata has been synchronized (this element is used to return the IsNew property).
[Visual Basic 6]

If pXMLPropertySet.IsNew Then
pXMLPropertySet.SetPropertyX "Esri/SyncOnce", "FALSE", esriXPTText, _
esriXSPAAddOrReplace, False
End If
After setting the SyncOnce element, you can use the FGDCSynchronizationHelper again. Calling the
FinishSynchronization method will update the ESRI metadata elements SyncDate, SyncTime, ModDate, and ModTime,
and the Metainfo\Metd element, for you.
[Visual Basic 6]

pSynchronizationHelper.FinishSynchronization pXMLPropertySet
To complete the Synchronize method, use the Metadata property to set the updated XmlPropertySet back to the
metadata file on disk.
[Visual Basic 6]

IMetadata_Metadata = pXMLPropertySet
Implementing IMetadataEdit
IMetadataEdit is a simple interface with one property, CanEditMetadata, which indicates if metadata is editable. This
interface is implemented by most GxObjects. For file-based GxObjects, such as the GxInterchangeObject, return True
if file permissions currently allow the metadata file to be edited.
[Visual Basic 6]

Private Property Get IMetadataEdit_CanEditMetadata() As Boolean


IMetadataEdit_CanEditMetadata = False
If ExistsMetadata(m_sFullName) Then
Dim file As Object
Set file = m_pFileSystemObject.GetFile(m_sFullName)
If Not file.Attributes = 1 Then
IMetadataEdit_CanEditMetadata = True
End If
End If
End Property
For GxObjects that reside in a geodatabase, CanEditMetadata should indicate if the user has the appropriate database
rights to edit the metadata stored in the geodatabase, not just to view it.
IMetadataEdit is generally implemented by GxObjects which reside in a geodatabase.
Adapting IGxObjectEdit members to account for metadata
If you do implement metadata for your GxObject, you should account for the presence of metadata files in the Delete
and Rename members of IGxObjectEdit.
[Visual Basic 6]

Private Sub IGxObjectEdit_Delete()


...
m_pFileSystemObject.DeleteFile (m_sFullName)
If m_pFileSystemObject.FileExists(m_sFullName & ".xml") Then
m_pFileSystemObject.DeleteFile m_sFullName & ".xml"
End If
...
Now that your GxInterchangeObject is complete, you must create an object factory class to allow ArcCatalog to
instantiate GxInterchangeObjects. You may also want to review the Creating other kinds of GxObject and
GxObjectFactory section for advice on adapting this example and implementing other GxObject interfaces.

226

Creating a subtype of GxObjectFactory

To use a new GxObject with ArcCatalog, you need to create a GxObjectFactory. ArcCatalog will use the
GxObjectFactory to check if any of the associated data exists in a given folder. The factory is also used to instantiate
the GxObjects to represent that data.
GxObjectFactories are registered to the ESRI Gx Object Factories component category.
A GxObjectFactory requires only one interface to be implemented, which is IGxObjectFactory.

Creating the GxInterchangeFactory

The naming convention for GxObjectFactories is the name of the GxObject it creates, with a suffix of `Factory', in this
case GxInterchangeFactory. In the same project as the GxInterchangeObject, add a new class called
GxInterchangeFactory, and implement IGxObjectFactory.
The key to writing the factory class is being able to identify your data type and, hence, being able to identify if the files
on disk represent the data in question. In this example, the file format consists of one file, which is simple to identify
from its .e00 extension. However, in some cases the situation is more complex.
For example, the shapefile format is made up of three main files with the extensions .shp, .shx, and .dbf and is filtered
by the GxFilterShapefiles coclass. In addition, .dbf (dBase) files can also be displayed in ArcCatalog as separate tabular
datafiles. If any of these files are missing, the format becomes invalid, and the filter coclass represents this by
displaying a different icon to indicate the status to the user. In addition to this issue, a dBase file is valid as a separate
tabular dataset, filtered by the GxFilterdBASEFiles filter class.
Therefore, if you intend to adapt this example and create a GxObject and GxObjectFactory for another data format,
the filtering rules you must apply may be more complex and may affect other data sources; ensure you can
adequately filter your data format before deploying your solution.
Implementing IGxObjectFactory
IGxObjectFactory provides all the functionality necessary for a GxObjectFactory.
From the Name property, return a string describing the type of files that the objects represent.
[Visual Basic 6]

Private Property Get IGxObjectFactory_Name() As String


IGxObjectFactory_Name = "Interchange (.e00) Files"
End Property
The write-only Catalog property is called by ArcCatalog when it creates the factory and passes in a reference to the
current GxCatalog object. In the GxInterchangeFactory, this reference is not actually required by any other members,
and the object reference is stored as a member variable in the factory object.
[Visual Basic 6]

Private Property Set IGxObjectFactory_Catalog(ByVal RHS As IGxCatalog)


m_pCatalog = RHS
End Property
If you adapt this example for another purpose or expand this sample, you can use the reference to the GxCatalog if
you need to access other items in the catalog from inside your factory class.
ArcCatalog calls the HasChildren method of each GxObjectFactory to check if any files of the relevant type are present
in a specific folder; the result of HasChildren determines whether or not it is necessary to call GetChildren. This design
is used to increase the display speed of folders in ArcCatalog. You should, therefore, ensure your HasChildren method
runs as efficiently as possible.
In the GxInterchangeFactory code below, the FileNames parameter passed in to HasChildren is iterated and checked
for any filenames with an extension of .e00. Once an InterchangeFile is found, the return value is set to True, and the
function exits.
[Visual Basic 6]

Private Function IGxObjectFactory_HasChildren(ByVal parentDir As String, _


ByVal FileNames As esriSystem.IFileNames) As Boolean
Dim sName As String
Do
sName = FileNames.Next
If sName <> "" Then

227

If UCase(Right(sName, 4)) = ".E00" Then


IGxObjectFactory_HasChildren = True
Exit Do
End If
End If
Loop Until sName = ""
End Function
The GetChildren method is used to return an enumeration of GxObjects to ArcCatalog. The same parameters are
passed to GetChildren as were passed to HasChildren. In the GetChildren method, the FilesNames are again iterated,
but this time each filename with a .e00 extension is used to create a GxInterchangeObject. These objects are stored in
an array and returned when the function is finished.
[Visual Basic 6]

Dim pChildren As IGxObjectArray


Set pChildren = New GxObjectArray
Do
sName = FileNames.Next
If UCase(Right(sName, 4)) = ".E00" Then
Dim pChild As New GxInterchangeObject
pChild.Name = sName
pChildren.Insert -1, pChild
Set pChild = Nothing
FileNames.Remove
End If
Loop Until sName = ""
Set IGxObjectFactory_GetChildren = pChildren
In the above code, the factory is calling the nonpublic Name property let, to inform the GxInterchangeObject of its
location. After ArcCatalog receives the enumeration of child GxObjects, it will iterate the array and call each object's
Attach method.
Adapting IGxObjectFactory members to account for metadata
If a GxObject has accompanying metadata, the metadata file should not be displayed as a separate XML file in
ArcCatalog. As the GxInterchangeObject example allows for metadata to be created, you need to adapt the
GetChildren method. Find XML files that have the same name as existing Interchange files, and remove these files
from the FilesNames enumeration.
[Visual Basic 6]

Dim bFoundXML As Boolean


Do
bFoundXML = False
sName = FileNames.Next
If UCase(Right(sName, 4)) = ".XML" Then
bFoundXML = True
sName = Left(sName, Len(sName) - 4)
End If
If UCase(Right(sName, 4)) = ".E00" Then
FileNames.Remove
If Not bFoundXML Then
Dim pChild As New GxInterchangeVB.GxInterchangeObject
pChild.Name = sName
...
In the code above, the .xml file extension is removed from each FileName, and if the remainder of the FileName
indicates an Interchange file, the FileName is removed from the enumeration; then the GxInterchangeObject is
created as long as the current FileName indicated the Interchange file itself and not the accompanying XML file.

Plugging GxInterchangeObject into ArcCatalog


Once the component is compiled, you need to register the GxInterchangeFactory to the ESRI GX Object Factories
component category. See the 'Component Categories' section in Chapter 2 for more information on how you can
register to component categories.
Open ArcCatalog, and you should now be able to see your Interchange files in the Catalog. Try copying and pasting an
Interchange file. You can also view the Metadata in the usual way.

228

See Also About GxObjects and GxObjectFactories, Creating other kinds of GxObject and GxObjectFactory, and
Interchange GxFilter Example.

Creating other kinds of GxObject and GxObjectFactory


The GxInterchangeFile demonstrates only one possibility for a custom GxObject. You may also want to implement the
other interfaces, which are optionally implemented by GxObjects, or create a specialist type of GxObject.
IGxObject name properties for other types of GxObject
The Name, FullName, and BaseName properties of IGxObject were discussed as part of the GxInterchangeObject
example; respectively, these properties should return the filename, filename with path, and filename with no
extension.
However, if you create GxObject, which does not relate specifically to a single file on disk, you may be unsure what to
return from these basic properties.
Generally, the Name property should identify the object from the other objects in the same GxObjectContainer. For the
GxInterchangeFile example, as for most GxObjects relating to a single file, Name is the filename including extension.
For a FeatureClass in a personal geodatabase, the Name indicates the name of the FeatureClass; for an enterprise
geodatabase, Name comprises the owner plus the name of the FeatureClass, for example, UserName.MyFeatureClass.
BaseName is generally used for file-based data and returns the name of the file without the extensionfor example, a
shapefile called MyShape.shp would have a BaseName of MyShape.
The FullName property should include enough information to be able to identify the item completelypassing the
FullName to the IGxCatalog::GetObjectFromFullName should return the correct GxObject. Again, for most GxObjects
relating to a single file, FullName indicates the full path and filename.
C:\Temp\MyShapefile.shp

The FullName of a shapefile is the full filepath.


For a personal geodatabase feature class, FullName consists of the full path to the .mdb file, plus the name of the
feature class, or the name of the dataset plus feature class.
C:\Temp\MyPGDB.mdb\DatasetNorth\FeatureClassNorthWest

229

The FullName of a personal geodatabase feature class includes the filepath, the dataset if present,
and the name of the feature class.
An enterprise geodatabase feature class however indicates its FullName as the name of the file the connection is
stored in, followed by the name of the dataset (if present) and name of the feature class.
Database Connections\MySDE\SHELLY.Dataset\SHELLY.FeatureClass

The FullName of an enterprise geodatabase feature class includes the file path, the dataset if
present, and the name of the feature class.
The database connections files are stored in your profile, and have the same name as shown in the ArcCatalog tree
view.

IGxObjectEdit::EditProperties
The GxInterchangeObject example displays the standard windows File Properties dialog box in response to the
EditProperties method. However, if this is insufficient for your needs, you can instead create a PropertySheet and add
custom property pages to allow your users to edit whichever custom properties you require.
If your GxObject defines and implements its own interface to allow access to nonstandard functionality, this would be
the ideal way to allow access to these methods through the user interface.
If your GxObject has its own specialist functionality, displaying custom property pages in response to
the EditProperties method is the ideal way to allow users access to this functionality.
GxObjectContainers
GxObjectContainers are GxObjects with GxObject descendants of their ownfor example, a folder on disk is
represented by a type of GxObjectContainer, specifically a GxFolder.
In many ways, a GxObjectContainer acts much the same as any other GxObject. First, the Catalog checks with all
registered GxObjectFactories and creates GxObjects as required. After each GxObject is attached, Catalog will check if
the object supports IGxObjectContainerif so the GxObject will be treated slightly different, as described below in the
'Implementing IGxObjectContainer' section.
GxObjectContainers are GxObjects which have child GxObjects attached to them; generally they do
not represent actual datasets.
Creating a GxObjectContainer
If you create a GxContainerObject, you should generally implement the standard IGxObject, IGxObjectUI, and
IGxObjectEdit interfaces. It is your choice as to whether the Copy, Rename, Delete, or EditProperties methods also
account for the container's Children; this is generally determined depending on what the container represents. For
example, deleting a GxFolder will logically also delete all the files the folder contains and, therefore, all the GxObject
children; by editing the properties of a GxFolder, you can also apply properties, such as read-only, to the files and
folders the object contains.
The code below demonstrates a fictitious example of a GxObjectContainer, GxExcelObject, which allows you to browse
to a Microsoft Excel spreadsheet file and the individual sheets contained in the file. Note that such code would need to
use the Microsoft Excel 9.0 Object Library (Excel9.olb).
Implementing IGxObjectContainer
To implement HasChildren, work out if any child GxObjects exist, based on your knowledge of the container type. For
example, a GxFolder will use its FullName to check a folder on disk and see if it contains any files.
Your GxObjectContainer may be based on a data format, which is not supported in ArcGIS and, therefore, may contain
other custom GxObjects that you have created. If, however, you want to add standard GxObjects to your container,
you can QI to IGxFactories on the Catalog to gain access to all the enabled factories. Check each factory using
HasChildren, and if appropriate, access the Children and Attach each one to the container. For example, a container
representing a zipfile might contain any type of GxObject. If HasChildren returns True, the GxContainerObject will be
displayed in ArcCatalog with a 'plus' sign alongside, indicating to the user that they can drill down to GxObjects below.

If a GxObjectContainer returns True from HasChildren, the Catalog will display an icon enabling the
user to drill down and find the child GxObjects.
If the user chooses to display a container's child objects, the Catalog will call IGxObjectContainer::Children. You can

230

build and return an enumeration of child GxObjects by using a GxObjectArray. This class is specifically designed for
use by a custom GxObjectContainer.
A GxObjectContainer is responsible for attaching and detaching its own child objects. Call Attach on each child as you
add it to the Children enumeration. The code below demonstrates how the GxExcelObject builds its Children
enumeration
[Visual Basic 6]

Set m_pChildren = New GxObjectArray


Dim i As Integer, pObj As GxExcelSheetObject, pGxObj As IGxObject
If Not m_Spreadsheet Is Nothing Then ' m_Spreadsheet references an Excel workbook.
For i = 1 To m_Spreadsheet.Worksheets.Count
Set pObj = New GxExcelSheetObject
' m_sFullName is a string holding the path of the excel file.
pObj.Name = m_sFullName & "\" & m_Spreadsheet.Sheets(i).Name
Set pGxObj = pObj
pGxObj.Attach Me, m_pCatalog
m_pChildren.Insert i - 1, pObj
Next i
End If
Add code to your Detach method to call Detach on each child GxObject, before releasing the container's own Parent
and Catalog references.
[Visual Basic 6]

If Not m_pChildren Is Nothing Then


Dim pEnumChildren As IEnumGxObject, pGxObject As IGxObject
Set pEnumChildren = m_pChildren
pEnumChildren.Reset
Set pGxObject = pEnumChildren.Next
Do While Not pGxObject Is Nothing
pGxObject.Detach
Set pGxObject = pEnumChildren.Next
Loop
m_pChildren.Empty
End If
It is not essential to implement AddChild and DeleteChilda Refresh of the container will pick up the new child
GxObjects automatically. If you do implement these methods, don't forget to Attach the new Child to its parent (the
current instance of the GxObjectContainer) in AddChild and Detach in DeleteChild.
You may want to use the IGxSelection::DelayEvents method to delay the processing of any events until changes to the
container are finished.

Adding object caching


Most GxObjectContainers also provide other standard functionalityfor example, caching, via the IGxCachedObjects
interface. Object caching may be implemented by any GxObject (not just containers) to increase efficiency, particularly
if a time-consuming operation is required to work out the properties of that GxObject.
For example, a GxDatabase needs to connect to the database and iterate the dataset names in the database to return
its Children. As this operation may be time-consuming, the dataset names are cached as lightweight Name objects.
Implementing IGxCachedObjects
Implementing IGxCachedObjects adds caching behavior to a GxObject. In the LoadChildren method, you should load
the objects or items you want to cache. For example, for the fictitious GxExcelObject, LoadChildren could open the
spreadsheet, read the names of the individual sheets, and create GxObject children for each sheet. These children
would then be returned as required, instead of being created each time the IGxObjectContainer::Children property
was called.
IGxCachedObjects gives a GxObject the opportunity to cache required resources.
To implement ReleaseCachedObjects, you must release all your references to objects you are caching. QI each child
GxObject for IGxCachedObjects and call its ReleaseCachedObjects method; then release the reference to all the child
objects by emptying the GxObjectArray.
[Visual Basic 6]

Dim i As Integer, pCachedObjects As IGxCachedObjects


For i = 0 To i < m_pChildren.Count
If TypeOf m_pChildren.Item(i) Is IGxCachedObjects Then
Set pCachedObjects = m_pChildren.Item(i)
pCachedObjects.ReleaseCachedObjects

231

Set pCachedObjects = Nothing


End If
Next i
Don't forget to also release any other cached items that you will re-create in your caching routine.
If implementing caching in a GxObjectContainer, you should force a release and recache all the child GxObjects in the
IGxObject::Refresh to ensure data is updated.
[Visual Basic 6]

Private Sub IGxObject_Refresh()


ReleaseChildren
CacheChildren
End Sub
If you have implemented caching on any GxObject, it would be advisable to release the cached objects before
performing a Delete. If your class is a GxObjectContainer, you may want to ensure all the child GxObjects are also
released before performing the delete of the container. Both these requirements should be covered by calling the
class's IGxCachedObjects::ReleaseCachedObjects method.
Root objects
The icons that appear as children of the Catalog in the ArcCatalog tree view are all GxRootObjects. All the existing root
objects are also GxObjectContainersthey provide top-level access to other GxObjects. Some GxRootObjects provide a
shortcut to folders where geographic data or related files are storedfor example, the Coordinate Systems
GxRootObject (GxSpatialReferencesFolder) provides quick access to the folder in your ArcGIS installation that contains
predefined coordinate system files.

GxRootObjects are GxObjects registered to the ESRI Gx Root Objects component category.
GxRootObjects appear directly beneath the Catalog icon and provide quick access to different types
of resources.
Creating a GxRootObject
You can create your own GxRootObject to provide top-level access to any folder, files, or other objects you want. First,
create a GxObject and register this class to the ESRI Gx Root Objects component category. When ArcCatalog starts
up, it will instantiate one instance of each of the classes registered to this category. A GxRootObject needs to know its
own Name, as it does not have a Factory object to Name it. Set up the object's Name in the class initialization code;
the name may be a constant location or a dynamic one, perhaps stored in the registry.
Root objects as containers
Generally, GxRootObjects are also GxObjectContainers (they implement IGxObjectContainer) although this is not
mandatory. If you do implement IGxObjectContainer on a root object, you will need to ensure that the object knows
about its Children. You might want to add this code to the object's Attach method or class initialization code. For
example, the GxRemoteDatabaseFolder root object will always have the same Name, pointing to the same location in
the ArcGIS install, from which it will add a child GxObject for each .odc file. These files are where the database
connection information is stored.
A GxDiskConnection is a GxRootObject, which can be defined by a user to provide a shortcut to any local or remote
folderGxDiskConnection Names are persisted by ArcCatalog and used to create the array of child GxObjects.
Instead of using a context menu with an option to create a new item, some GxRootObjects provide a special child
GxObject to allow a user to create a new item.

232

You may also want to implement IGxCachedObjects; most GxRootObjects implement this interface to ensure the tree
view in ArcCatalog displays efficiently.

GxObjects with wizards


The Address Locators (GxLocatorFolder) root object provides not only a shortcut to a folder but also an extra child
GxObject which can be used to create a new instance of a locator (an alternative to allowing users to create a new
GxLocator via a context menu). The Data Connections (GxRemoteDatabaseFolder) and GIS Servers
(GxGISServersFolder) root objects also provide similar functionality for creating new connections to different types of
geographic data and services.
When any GxObject is double-clicked, Catalog will QI for IGxObjectWizard. If the QI is successful (for example, for a
GxLocatorFolder), it will then call the Invoke method.
You could add this type of functionality by creating a GxObject with a constant Name. The object does not represent
data and does not need to account for a different Name. You do not need to create a corresponding factory class;
instead, add a single instance of the wizard GxObject to the GxRootObject when the GxRootObject is initialized.
Implementing IGxObjectWizard
Implement IGxObjectWizard on your GxNewObject and use its single method, Invoke, to allow a user to create a new
child object as required. You could present a dialog box or wizard to the user or have some kind of automated creation
of a new GxObject.
If a GxObject implements IGxObjectWizard, then Catalog will call the Invoke method when a user
double-clicks on that GxObject.
Implementing IGxPasteTarget
IGxPasteTarget should only be implemented on GxObjectContainers. It is used to enable drag-and-drop functionality.
IGxPasteTarget allows drag and drop to attempt a paste operation on a GxObject.
IGxPasteTarget cannot be implemented in VB. If implementing IGxPasteTarget in VC++, check the NamesEnumerator
passed in to the CanPaste method. If one or more of the Names can be a child of your GxObjectContainer, return
True; this will enable the use of Ctrl+V and the Paste item on the Edit or context-sensitive menu (if present).
Implementing IGxThumbnail
IGxThumbnail can be used to provide a small overview picture of a file's contents when the user chooses the
Thumbnails view. A GxObject, which implements IGxThumbnail is generally one that represents a file with pictorial
content (for example, a Map document). Such a file may contain a suitable thumbnail picture embedded within the
main filefor example, EPS files can normally be saved with a small TIFF thumbnail embedded in the header of the
file.
IGxThumbnail provides access to the picture shown in the thumbnail's contents view.

IGxThumbnail is a straightforward interface. Return an IPicture variable (defined in the Microsoft Standard OLE Types
object library) from the Thumbnail property; you should also provide the ability to set this property.

233

If your GxObject represents a file that has an embedded thumbnail view, you may want to open the file and read or
write the embedded thumbnail.

Synchronizing metadata
Each GxObject decides which elements to update during synchronization and how to collect and update the relevant
synchronized information. The GxInterchangeObject code demonstrates one possibility for synchronization.
Using an FGDCSynchronizationHelper
You may want to use the FGDCSynchronizationHelper to add boilerplate information to your metadata. Note that the
code below only Updates Boilerplate elements the first time the metadata is synchronized (when IsNew equals True).
[Visual Basic 6]

If pXMLPropertySet.IsNew Then
Dim emptyVar As Variant
pMetadataSynchronizer.Update pXMLPropertySet, "Boilerplate", emptyVar
...
This code will add to the XMLPropertySet many standard metadata elements, which will be FGDC standard elements.
These elements will be automatically completed with 'hint' values. You may have seen these hint values when creating
metadata for existing GxObjectsfor example, the Abstract element (idinfo/descript/abstract) will be added, complete
with the initial value "REQUIRED: A brief narrative summary of the data set.".
For a full list of which metadata elements will be added by boilerplate elements, see the white paper Synchronization
in ArcCatalog, which can be found on the ArcObjects Online Web site
If you do intend to apply the complete FGDC standard to the metadata, you may want to use the
FGDCSynchronizationHelper Populate members to help complete synchronization, in particular
PopulateStaticProperties, which completes not only the boilerplate elements but also the DataSetName,
OperatingSystem, Environment, Software, Language, and MetadataStandard elements automatically. For full details on
which elements will be updated by using the Populate methods, again see the white paper Synchronization in
ArcCatalog. You should also refer to this paper for general information about completing metadata to the ESRI Profile
of the FGDC standard.
Updating other elements
You may want to synchronize metadata indicating the GxObject's file size by updating the DatasetSize element.
[Visual Basic 6]

Dim file As Object, vSize As Variant


Set file = m_pFileSystemObject.GetFile(m_sFullName)
vSize = Format(file.Size / 1048576, "0.000")
pMetadataSynchronizer.Update pXMLPropertySet, "DatasetSize", vSize
Above, the file size is calculated in megabytes, which is the form expected by the FGDC standard. If the GxObject
supports IGxObjectProperties, you may be able to retrieve the file size by calling GetProperty using the string
ESRI_GxObject_FileSize (note that not all GxObjects will support this property).
The GxInterchangeObject updates the Environment metadata element with the information about the current
operating system; however,, according to the FGDC standard, this should also include the software name. You could
extend the GxInterchangeObject implementation to include this information by reading the software name and version
from the file resource strings of AfCore.dll by using the steps below.
1.

Locate the installation path of ArcGIS by using the executable path of the ArcCatalog application, or
alternatively, append "bin\" to the path in the registry key H_L_M\SOFTWARE\ESRI\CoreRuntime\InstallDir.
(Note that versions of ArcGIS prior to 9.x may have different registry entries.)

2.

Form a file pathname to AfCore.dll.

3.

Use Win32 API calls GetFileVersionInfoSize(), GetFileVersionInfo(), and VerQueryValue()to read the Product
Name and ProductVersion resources from AfCore.dll.

If you do not make use of the FGDCSynchronizationHelper FinishSynchronization method, you should still ensure the
MetadataDate elements (listed above) are updated every time you perform synchronization, for example:
[Visual Basic 6]

Dim vDate As Variant


vDate = CVar(Format(Now, "yyyymmdd"))
pMetadataSynchronizer.Update pXMLPropertySet, "MetadataDate", vDate

Creating other kinds of GxObjectFactories


The GxInterchangeFactory implements the basic interface required for GxObjectFactory functionality. If you are
designing a different kind of factory, you may also want to implement the interfaces discussed below, depending on
the design of the class and its related GxObject.
Implementing IGxObjectFactoryEdit
By implementing this interface, you can offer users the ability to change how a GxObjectFactory functions. For
example, the GxTextFileFactory implements IGxObjectFactoryEdit, allowing users to select which file extensions should
be represented by a GxTextFile object.

234

In the ArcCatalog Options dialog box, there is a list of all the current factories. If the selected factory implements
IGxObjectFactoryEdit then selecting the factory in the list enables the Properties button. Clicking the button calls the
EditProperties method.
To implement the EditProperties method, display a form allowing the user to change some internal properties of the
GxObjectFactory.

By implementing IGxObjectFactoryEdit, you can provide tools to allow users to change how a
GxObjectFactory operates.
For example, you could use IGxObjectFactoryEdit to enable or disable more stringent file content checking, allowing
the user to choose their priorityquicker code or more robust file checking.
Implementing IGxObjectFactoryMetadata
If your GxObject supports metadata (implements IMetadataEdit and IMetadata), you should implement
IGxObjectFactoryMetadata on its associated factory object.
GetGxObjectFromMetadata is straightforward to implement; you simply need to calculate the path of the
accompanying GxObject from the path of the metadata file, then instantiate and return that GxObject.
Generally, the metadata file sits adjacent to the GxObject file with the same BaseName. For example, a shapefile
called Cafe.shp will have an adjacent file called Cafe.shp.xml.
Implementing IGxObjectFactoryPriority
The Catalog creates a list of GxObjectFactories from the ESRI Gx Object Factories component category, and
consequently, those with a lower CLSID will be found first. This means that when retrieving the Children for any
GxObjectContainer, those factories with a lower CLSID value will be used before those with a higher value.
To specifically change the order in which your GxObjectFactory will be asked for its Children, you can implement
IGxObjectPriority. Priorities below 0 mean that the factory will be used after all the factories with no specific priority;
Priorities above 0 mean that the factory will be used before all the factories with no specific priority. For example, the
GxPrjFileFactory has a priority of -100. This ensures that it is used after the GxShapefileFactory, and, therefore, any
.prj files that accompany shapefiles are not displayed as separate GxPrjFile objects. Generally, a negative priority
should be used, so as not to change the behavior of the Catalog.
See Also About GxObjects and GxObjectFactories, Interchange GxObject Example, and Interchange GxFilter Example.

235

GxFilter Interchange Files Example

Example Code Click here


Description This project provides a GxObjectFilter, which can be used to browse to and select ArcInfo Interchange
files (.e00) in the GxDialog.
Design GxFilterInterchangeFiles class inherits from GxObjectFilter.
License ArcView or above.
Libraries Catalog, CatalogUI, and GxInterchangeVB (example project).
Languages Visual Basic
Categories ESRI GX Object Filters
Interfaces IGxObjectFilter
How to use
1.

If using VB, register GxFilterInterVB.dll and double-click the GxFilterIntVB.reg file to register to component
categories.

2.

Open ArcMap, click Tools, click Macros, then click Visual Basic Editor.

3.

Click Tools click References, then in the References dialog box, browse to the sample GxFilterIntVB.dll and
click OK. Then choose the GxFilterInterVB reference in the list before clicking OK to add the reference to
the VBA project.

4.

Paste the following code into the VBA window to open a GxDialog, which uses the GxInterchangeFilter.
[Visual Basic 6.0]

Sub OpenDialog()
Dim pGxDialog As IGxDialog, pFilterColl As IGxObjectFilterCollection
Set pGxDialog = New GxDialog
pGxDialog.Title = "Interchange files"
Set pFilterColl = pGxDialog
Dim pGxFilter As IGxObjectFilter
Set pGxFilter = New GxFilterIntVB.GxFilterInterchange
pFilterColl.AddFilter pGxFilter, True
Dim pEnumGxObj As IEnumGxObject
pGxDialog.DoModalOpen 0, pEnumGxObj
End Sub
5.

Close the Visual Basic for Applications window and return to ArcMap.

6.

Click Tools, click Macros, click Macros again, then choose the OpenDialog macro and click Run.

7.

Browse to an ArcInfo Interchange (.E00) file.


The GxDialog will allow you to browse to and select an Interchange file. You can add other custom
functionality according to how you want to be able to use the Interchange file.

The case for a GxFilterInterchangeFiles class


ArcGIS applications use a mini browser in many areas to browse the file system and select files. This browser has been
designed to be similar to the Microsoft Common Dialog Control, but to work specifically with geographic data.

236

The browser is supplied with classes for filtering on many different types of geographic datafor example, coverages,
shapefiles, and CAD files; personal and enterprise geodatabases; datasets, annotation, feature classes, and so on.

The range of geographical data types, which can be displayed by the GxDialog is extensive but not exhaustive.
However, the dialog box is designed to be extensible to allow you to display additional formats or types. In particular,
you may want to create a GxObjectFilter to accompany a custom GxObject.
GxObjectFilters in the GxDialog
By reviewing the ArcCatalog object model diagram, you can see that the GxDialog consists of two main parts. The
GxDialog browser class itself can display the contents of one GxObjectContainer at a time.

The GxDialog contains a collection of GxObjectFilters that are added by the client. Only one filter is active at any one
timethe filter currently selected in the 'Show of Type' dropdown box.
How the GxDialog uses GxObjectFilters
Each of the GxObjectFilters is responsible for deciding whether a certain type of GxObject can be shown and selected
in that GxDialog.
For example, when DoModalOpen or DoModalSave are called, the GxDialog checks each GxObject against the active
GxObjectFilter's CanDisplay method and only displays those that return True.
The GxDialog uses the CanChooseObject method to determine if a user can select a given GxObject and dismiss the
dialog box opened by DoModalOpen; the CanSaveObject method determines the behavior of the dialog box opened by
the DoModalSave.
Creating a subtype of GxObjectFilter

The majority of GxObjectFilter classes implement only a single interface, IGxObjectFilter, and therefore the
functionality of the browser can be extended quite simply to work with other data types.

Creating the GxFilterInterchange

237

In this example, you will create a class called GxFilterInterchange, which provides a GxObjectFilter for ArcInfo
Interchange files, to accompany the custom GxInterchangeObject developed in the previous example. This will allow
the display and selection of a GxInterchangeObject from a GxDialog.
Implementing IGxObjectFilter
IGxObjectFilter is used by the GxDialog to identify the data type to be displayed and selected.
Classes that implement IGxObjectFilter can be used by the GxDialog to identify which data can be
displayed in the dialog box.
Both of the properties, Name and Description, are used to provide information to the GxDialog, and do not affect the
filtering process. Name is used to identify this filter within the collection of filters for the GxDialog. In this example, it
returns "Interchange File Filter". Description is the string that appears in the GxDialog's dropdown list of data types. In
this example, it returns "Interchange files (.E00)".
To complete the methods, you will make use of the GxFilterBasicTypes class. This class is used inside most
GxObjectFilters. Instantiate the object in your class initialization code.
[Visual Basic 6]

Dim m_pBasicFilter As esriCatalogUI.IGxObjectFilter


...
Private Sub Class_Initialize()
Set m_pBasicFilter = New esriCatalogUI.GxFilterBasicTypes
End Sub
In your CanDisplay method, forward the GxObject reference received to the CanDisplay method of the basic filter. By
doing this, you ensure that container types, such as Catalog, GxDiskConnection, GxFolder, and so on, can be displayed
by your filter. This allows users to navigate around their data when your filter is active in the GxDialog.
[Visual Basic 6]

Private Function IGxObjectFilter_CanDisplayObject(ByVal Object As _


esriCatalogUI.IGxObject) As Boolean
IGxObjectFilter_CanDisplayObject = False
If m_pBasicFilter.CanDisplayObject(Object) Then
IGxObjectFilter_CanDisplayObject = True
Else
IGxObjectFilter_CanDisplayObject = IsE00File(Object)
End If
End Function
If the GxObject is not a basic type, you should next check if it is the type of GxObject your filter is concerned with.
Create the IsE00File function to check this, so you can also call the function from other methods. You will need to add
a reference to the GxInterchangeVB.dll component you created in the last example.
[Visual Basic 6]

Private Function IsE00File(ByVal Object As esriCatalogUI.IGxObject) _


As Boolean
IsE00File = False
If TypeOf Object Is GxInterchangeVB.GxInterchangeObject Then
IsE00File = True
End If
End Function
Call the IsE00File function from CanChooseObject, as users should be able to select any GxInterchangeObject with this
filter. You can determine if a double-click will select an object or not by setting the result parameter to any
esriDoubleClickResult value.
[Visual Basic 6]

Private Function IGxObjectFilter_CanChooseObject(ByVal Object

As _

esriCatalogUI.IGxObject, result As esriCatalogUI.esriDoubleClickResult) _


As Boolean
result = esriCatalogUI.esriDoubleClickResult.esriDCRDefault
IGxObjectFilter_CanChooseObject = IsE00File(Object)
End Function
This code will display and also allow a user to choose, any file with the extension .E00. You could if you wished, adapt
this method to provide further checking if required. For example, you might want to check the IsValid property of the
GxObject before allowing it to be opened. Alternatively, a filter could be coded to work with a particular custom tool,
as you can allow or disallow the choice of a GxObject based on any appropriate criteria.
CanSaveObject may not be applicable to a GxInterchangeObject, as ArcGIS Workstation is generally used to create
interchange files, so you could simply return False from CanSaveObject. However, if you want to allow a custom tool
to use the filter to save to an interchange file, there are a number of parameters passed to CanSaveObject to help you
decide the return value.

238

Location contains the GxObjectContainer which will be the Parent of the proposed new GxObjectcheck that the
container is the correct type to contain a GxObject of the new type. For example, a GxLayer can be saved to a
GxFolder, but not to a GxCoverageDataset.
[Visual Basic 6]

If TypeOf Location Is IGxFolder Then


IGxObjectFilter_CanSaveObject = True
End If
Check the newObjectName parameterit may not have an extension, or it may have an incorrect extension for the
type of GxObject.
[Visual Basic 6]

Dim sExt As String


sExt = GetExtension(newObjectName)
If Len(sExt) = 0 Then
newObjectName = newObjectName & ".E00"
ElseIf Not UCase(sExt) = ".E00" Then
newObjectName = Left(newObjectName, Len(newObjectName) - Len(sExt)) _
& ".e00"
End If
Then you can use the name and the Location to determine if the GxObject already exists and set the
objectAlreadyExists parameter.
CanSaveObject will be called if the browser is being used to specify a folder and name for an output
file when the GxDialog has been opened using DoModalSave instead of DoModalOpen.
CanSaveObject will only be checked if CanChooseObject has previously returned True for that
GxObject.
[Visual Basic 6]

Dim sFullPath As String


sFullPath = Location.FullName & "\" & newObjectName
objectAlreadyExists = m_pFileSystemObject.FileExists(sFullPath)
All that remains is to register the new coclass to the ESRI Gx Object Filters component category. This will allow
ArcCatalog to automatically find, and users will be able to search for and select ArcInfo Interchange files, in addition to
the standard files, in the GxDialog.
GxObjectFilters can be registered to the ESRI Gx Object Filters component category.
Restricting the GxDialog to show only interchange files
If you create a GxDialog programmatically, you can choose which files it should display. For example, to use your new
GxObjectFilter to browse for an Interchange file with the GxDialog, first create the GxDialog, and QI to
IGxObjectFilterCollection.
[Visual Basic 6]

Dim pGxDialog As IGxDialog, pFilterColl As IGxObjectFilterCollection


Set pGxDialog = New GxDialog
pGxDialog.Title = "Interchange files"
Set pFilterColl = pGxDialog
Add the GxFilterInterchangeFiles filter.
[Visual Basic 6]

Dim pGxFilter As IGxObjectFilter


Set pGxFilter = New GxFilterIntVB.GxFilterInterchangeFiles
pFilterColl.AddFilter pGxFilter, True
Then display the GxDialog; the GxDialog will fill the IEnumGxObject with the selected file or files that can be extracted
for further use.
[Visual Basic 6]

Dim pEnumGxObj As IEnumGxObject


pGxDialog.DoModalOpen 0, pEnumGxObj

239

See Also About GxObjects and GxObjectFactories, Interchange GxFilter Example, and Creating other kinds of GxObject
and GxObjectFactory.

240

Chapter 7: Customizing the Geodatabase


Customizing the Geodatabase
The following sections provide examples of customizing the geodatabase object model.
Apart from these customizations it is not normally realistic to further extend the geodatabase model. Most of the
geodatabase objects implement behavior that is closely linked with the other objects in the model, so they are not
generally suitable for modification and reuse.
Class Extensions
About Class Extensions
Introduction to class extensions.
Pipe Validation Class Extension Example
An example of a class extension which provides custom validation of attributes
Managing Class Extensions
Advice on applying your class extension to your data
Timestamper Class Extension Example
An example of a class extension, which attributes a feature with an automatic time stamp when the feature is created
or edited
Class Extensions and Relationship Classes
Advice on using class extensions with relationship classes
Class Extensions for Annotation and Dimensions
Advice on extending annotation and dimension classes
Custom Features
About Custom Features
Introduction to custom features
TreeFeature Custom Feature Example
Example of a custom feature representing a tree
Custom Features versus Other Solutions
A discussion of when an alternative approach may be more appropriate than a custom feature
Making a class extension with your custom feature
Advice on making a class extension to accompany your custom feature
Managing Custom Features
Advice on creating data with your custom features and managing custom features with class extensions
Plug In Data Sources
About Plug-In Data Sources
Introduction to plug-in data sources
Simple Point Plug-In Data Source Example
An example of a plug-in data source providing access to a data format containing points
Other Plug-In Data Source Topics
Advice on programmatic access, catalog searches, and licensing for plug-in data sources
Workspace Extensions
About Workspace Extensions
Introduction to workspace extensions.
ConnectLog Workspace Extension Example
An example of a workspace extension, which records user connections to a database
Managing Workspace Extensions
Advice on deploying and using workspace extensions
OLE DB Providers
About OLE DB Providers
Introduction to OLE DB providers
OGIS OLE DB Provider Example
An example that implements a spatially enabled OLE DB provider for personal geodatabases

About Class extensions


Class extensions are the simplest and most important way of customizing geodatabase behavior. In particular, they
provide the following capabilities:

Complex validation rules (through IObjectClassValidation)

241

Handling of edit events (through IObjectClassEvents and IRelatedObjectClassEvents)

Enhancement of the Attributes dialog box (through the IObjectInspector interfacesee Chapter 8, 'Extending the
editor')

Customized rendering of feature classes (through the IFeatureClassDraw interfacesee Chapter 5, 'Extending
the display')

Automated creation of preconfigured tables and feature classes (through IObjectClassDescription and
IFeatureClassDescription)

Storage of arbitrary objects and data with an object class (through extension properties)
Class extensions are at their best when used for important business rules that can be simply
implemented without serious performance considerations.

These capabilities could often instead be provided in application code; for example the handling of edit events could be
implemented as an editor extension for the ArcMap application. There are many advantages and disadvantages of
class extensions relative to application customization. These are summarized in the following table.
Class extensions

Advantages

Application customization

Database customization is always available. It is


not dependent on a particular application such as
ArcMap being present. This can be important for
feature classes accessed from ArcGIS Engine or
ArcGIS Server.

Easy to implement and tightly integrated with


the application user interface.

Business logic is stored closely to the data.


A level of encapsulation is guaranteed.

The DLL is only required by those users who


need the specific customization functionality.

If customization fails then user can access


important data with other tools.

All ArcGIS users require access to the


customization DLL, even to view the data.

Disadvantages

If the customization fails at runtime, the data


cannot be accessed from ArcGIS (this can also be
considered an advantage that ensures data
integrity).
The developer cannot make any assumptions that
a particular application will be running. This can
limit functionality.
An object class can only have one class extension.

There is a possibility that users could avoid


business rules by running the application
without the customization.
Implementation is sometimes duplicated
among several applications.
The customization is only available when the
application is running.

You cannot easily extend annotation feature


classes or dimension feature classes.
See Also Pipe Validation Class Extension Example, Managing Class Extensions, Timestamper Class Extension Example,
Class Extensions and Relationship Classes, and Class Extensions for Annotation and Dimensions.

PipeValidation Class Extension Example

Description This project provides a custom validation of attributes, such that for any feature with a length greater
than 10 meters, the valid values for MATERIAL are 'Coated Steel' or 'PVC'.
Design Subtype of FeatureClassExtension abstract class

242

License ArcEditor or above


Libraries Geodatabase, Geometry, and System
Languages Visual Basic, Visual C++
Categories ESRI GeoObject ClassExtensions
Interfaces IClassExtension, IFeatureClassExtension, and IObjectClassValidation
How to use
1.

If using VB, register PipeValidationVB.dll, and double-click the PipeValidationVB.reg file to register to
component categories.
If using VC++, open and build the project PipeValidationVC.dsp to register the DLL and register to
component categories.

2.

Open ArcMap. If using VB, add the PipesVB feature class from the Extending ArcObjects sample data. If
using VC++, add the PipesVCpp feature class. These feature classes have been preconfigured with the
example's class extension.

3.

Start editing and select all features in the class. Click the Editor menu and click Validate Features. Two
features should be invalid. Select each feature individually and click Validate Features again. The reason
for invalidity will be shown.

The case for a Pipe validation class extension


Imagine a network of water pipes. Pipes longer than 10 meters may only be made of PVC or coated steel, but shorter
pipes may be made of many different materials. You would like to apply an attribute rule that ensures the material
type as just described.

Pipes longer than 10 meters may only be made of coated steel or PVC.
The valid materials are dependent on the pipe length. The usual way to implement dependent validation is with
subtypes, since each subtype within the object class can have a separate validation rule, and this can all be configured
in ArcCatalog without any programming. However, in the example the dependency is on the pipe length, which is not a
suitable attribute on which to base subtypes since there is no set of discrete values. A solution would be a custom
attribute rule that validates objects on a combination of fields (for example, length and material) rather than just one
field as normal. In the geodatabase the way to implement this behavior is with a class extension.

Implementing a class extension


Class extensions are the main way of providing custom geodatabase behavior. There are only two interfaces you must
implement: IClassExtension and IObjectClassExtension. The latter interface is trivial, it just provides an identity for
your extension. IFeatureClassExtension is a similar required identity interface if your extension applies to feature
classes only. If you don't implement IObjectClassExtension, your extension will still work, but it won't conform to what
is presented to developers on the ESRI object model diagrams.
Class extensions are not a way of making subclasses of the standard ObjectClass. Instead they
provide an extension to the capabilities of ObjectClass.
The COM class that implements the class extension must be registered to the ESRI GeoObject Class Extensions
component category. For details on how to apply a class extension to your data, see the next section in this chapter,
'Managing Class Extensions'.
Implementing IClassExtension
This is a simple interface, but possibly the most crucial. If there is an error in your code here, none of your users will
be able to access the data in the object class.
The Init method is fired every time your object class is opened. If your object class is contained within a feature
dataset, Init will fire as soon as any of the other feature classes are opened.
Within a feature dataset, if one feature class is opened, all the others are opened as well. This can
cause your class extension's Init method to fire when you might not expect it.
You will typically use Init to initialize objects you want to store at the class level. The Pipe Validation example stores
the index positions of the important fields to avoid recalculating them each time the field is used.
[Visual Basic 6]

Implements IClassExtension
Implements IObjectClassExtension
Implements IFeatureClassExtension
Implements IObjectClassValidation

243

Private m_iLengthField As Integer


Private m_iMaterialField As Integer
Private Const c_sMaterialField As String = "MATERIAL"
' HRESULT constant for returning errors
Private Const E_FAIL As Long = &H80004005
Private Sub IClassExtension_Init(ByVal pClassHelper _
As esriGeoDatabase.IClassHelper, _
ByVal pExtensionProperties As esriSystem.IPropertySet)
' Check that it is a linear feature class
' and that both length and material fields are present
Dim pFeatureClass As IFeatureClass
Set pFeatureClass = pClassHelper.Class
If pFeatureClass.ShapeType <> esriGeometryPolyline Then
Err.Raise E_FAIL, , "Not a linear feature class."
End If
Dim pLenField As IField
Set pLenField = pFeatureClass.LengthField
m_iLengthField = pFeatureClass.FindField(pLenField.Name)
m_iMaterialField = pFeatureClass.FindField(c_sMaterialField)
If m_iMaterialField = -1 Then
Err.Raise E_FAIL, , "Required field not found: " & c_sMaterialField
End If
End Sub
To run your class extension in the Visual Basic debugger, you will need a debug startup executable
that registers your class to the correct component categories, then starts the appropriate application
such as ArcMap.
For more details, see the description of the Compile and Register Add-In in the Component Categories section of
Chapter 2, 'Developing Objects'.
There are two parameters to Init. The second, the class extension properties, is discussed in the next example. The
first parameter, the class helper, is an intermediate object used to prevent circular references between an object class
and a class extension. You should not keep a class-level variable referring to the object class; instead, keep a
reference to this class helper object.
Note the error handling in the exampleno message boxes are used to report the errors. You should avoid all user
interface facilities in your class extension, since the geodatabase is independent of the user interface. Someone may
want to use your object class extension from a nongraphical environment such as the command line, in which case
message boxes would be inappropriate. It is better to pass the error back to the client application. In the example an
HRESULT error number is used. This means that clients to the class extension will be able to handle the error
appropriately whether they are written in Visual Basic or Visual C++.
Avoid unnecessary user interface functions in your class extension.
Implementing IObjectClassValidation
IObjectClassValidation provides custom validation of objects in addition to geodatabase validation of domains,
relationship rules, and connectivity rules. After successfully completing all native validation within the geodatabase,
the ValidateRow method is called. Effectively, this is the last type of validation performed when validating an object.
The ValidateRow method is called by an object's IValidate::Validate method and by the Validate methods on the
IValidation interface of the associated object class. When implementing ValidateRow you will typically pass on the
request to ValidateField, which provides the finer-grained validation.
[Visual Basic 6]

Private Function IObjectClassValidation_ValidateRow(ByVal Row As _


esriGeodatabase.IRow) As String
IObjectClassValidation_ValidateRow = _
IObjectClassValidation_ValidateField(Row, c_sMaterialField)
End Function
The ValidateField method is called when IValidate::GetInvalidFields is called on an object of the associated object
class. For both ValidateField and ValidateRow, if the field or row is invalid you should return an appropriate error
string; otherwise, return a zero-length string.
[Visual Basic 6]

Private Function IObjectClassValidation_ValidateField(ByVal Row As _


esriGeoDatabase.IRow, ByVal FieldName As String) As String
Dim sError As String
sError = ""

244

If FieldName = c_sMaterialField Then


Dim dLen As Double
Dim sMaterial As String
dLen = Row.Value(m_iLengthField)
If IsNull(Row.Value(m_iMaterialField)) Then
sMaterial = ""
Else
sMaterial = Row.Value(m_iMaterialField)
End If
If dLen > 10# _
And (sMaterial <> "PVC" And sMaterial <> "Coated Steel") Then
sError = "Value for " & c_sMaterialField & " is invalid." & _
vbNewLine & "If length is greater than 10m," & _
" only PVC and Coated Steel are valid."
End If
End If
IObjectClassValidation_ValidateField = sError
End Function
See Also About Class Extensions, Managing Class Extensions, Timestamper Class Extension Example, Class Extensions
and Relationship Classes, and Class Extensions for Annotation and Dimensions.

Managing class extensions


Once you have implemented your class extension, registered the DLL, and added the DLL to the ESRI GeoObject Class
Extensions component category, you will need a way of applying it to new or existing object classes.
When creating new object classes use one of the following methods to apply your class extension:

Use the Schema wizard after modelling your class extension in UML.

Use an ObjectClassDescription to automate the creation of new object classes in ArcCatalog (see the
Timestamper example later in this chapter).

Write code to call IFeatureWorkspace::CreateTable or IFeatureWorkspace::CreateFeatureClass, specifying your


class extension's GUID for the EXTCLSID parameter.
The GUID of your class extension can be found in the registry under HKEY_CLASSES_ROOT\<Your
ProgID>\CLSID. More simply, look for the GUID in the .reg script generated by the ESRI 'Compile
and Register' Visual Basic Add-in.

For existing object classes, apply the class extension by calling IClassSchemaEdit::AlterClassExtensionCLSID as shown
below.
[Visual Basic 6]

' QI for the IClassSchemEdit interface


Dim pClassSchemaEdit As IClassSchemaEdit
Set pClassSchemaEdit = pObjectClass
' set an exclusive lock on the class
Dim pSchLock As ISchemaLock
Set pSchLock = pObjectClass
pSchLock.ChangeSchemaLock (esriExclusiveSchemaLock)
' create the IUID object
Dim pCUID As IUID
Set pCUID = New UID
pCUID.Value = "PipeValidation.PipeClassExtension"
' alter the class extension for the class
pClassSchemaEdit.AlterClassExtensionCLSID pCUID, Nothing
' release the exclusive lock
pSchLock.ChangeSchemaLock (esriSharedSchemaLock)
The IUID interface will automatically convert a ProgID to the corresponding GUID.
You can remove the class extension by passing `Nothing' as the first parameter to

245

IClassSchemaEdit::AlterClassExtensionCLSID. However, if your class extension is faulty, you may not be able to open
the object class and so would not be able to QI to IClassSchemaEdit. In this situation use
IFeatureWorkspaceSchemaEdit to clear the class extension CLSID.
Note that an object class can only have one class extension. To combine two class extensions, you will need to merge
their source code.
See Also About Class Extensions, PipeValidation Class Extension, Timestamper Class Extension Example, Class
Extensions and Relationship Classes, and Class Extensions for Annotation and Dimensions.

Timestamper Class Extension Example


Object Model Diagram

Example Code Click here.


Description This extension writes date and time information whenever an object is created or changed. The field
names that are used to store the information are kept as properties of the extension, configurable via a property page.
A new feature class or table with this extension can be created with the ArcCatalog user interface.
Design Subtype of ObjectClassExtension abstract class
License required ArcEditor
Libraries System, Geodatabase, and Framework
Languages Visual Basic, Visual C++
Categories ESRI GeoObject ClassExtensions, ESRI GeoObject ClassDescriptions, ESRI Table Property Pages, and ESRI
Feature Class Property Pages
Interfaces IClassExtension, IObjectClassExtension, IObjectClassEvents, IObjectClassDescription,
IFeatureClassDescription, and IComPropertyPage

246

How to use
1.

If using VB, register TimestamperVB.dll, and double-click the TimestamperVB.reg file to register to
component categories.
If using VC++, open and build the project TimestamperVC.dsp, to register the DLL and register to
component categories.

2.

Using ArcCatalog, create a new feature class in a geodatabase of your choice. On the first page of the New
Feature Class dialog box, there is a combo box for the type of custom object you will store in the feature
class, choose 'Timestamped Feature Class'.

3.

After your feature class is created, right-click on it and choose Properties. Go to the Timestamping tab of
the Properties dialog box and inspect the settings.

4.

In ArcMap, start editing the feature class and digitize some new features. Inspect the attributes of the
features to see the timestamp details.

The case for a Timestamper class extension


Imagine that you would like to keep records on who has been editing your data. For your most important feature
classes and tables, you would like to know who created or modified any feature, and at what time the edit was made.
In summary, you would like to timestamp data changes.

Whenever a table is edited, you would like to record the date, the time, and the person who made
the change.
Timestamping can be achieved by customizing the editor to handle creation and modification events, but there may be
several different applications editing the dataeach would need to be customized. It may also be awkward to add new
feature classes to the scheme.
It is often preferable to implement this kind of business rule as close to the data as possible, to guarantee the rule is
enforced. Class extensions offer a way of implementing this kind of rule in the geodatabase. In this example, a class
extension solution is presented which uses IObjectClassEvents to trap creation and modification events. The solution
also presents the use of class extension properties and a property page to administrate the timestamping. The
IObjectClassDescription and IFeatureClassDescription interfaces are used so that a new timestamped table or feature
class can easily be created.
It is recommended that you run the Timestamper sample before reading the rest of the text, particularly to see the
property page in ArcCatalog.

Implementing a class extension with extension properties


You would like each timestamped object class to be able to use differently named fields to store the information. For
example, in one feature class, a field called CREATION_DATE may be specified, whereas in another the field may be
named CREATED. Additionally, you would like to be able to configure whether all the information is recorded. For
example, you may want to record the creation date, but not the modification date or username. This configuration
information belongs with the object class to which the extension is applied, so you need to store it within the
geodatabase in the same way as the object class's metadataextension properties are provided for this purpose. The
extension properties are stored within the geodatabase in the GDB_OBJECTCLASSES table.
A class extension may be applied to many different object classes. The extension properties allow
each of those object classes to be configured differently.
In the example's Init routine, the extension properties are copied into class-level variables so they can be manipulated
easily.
[Visual Basic 6]

Private Sub IClassExtension_Init( _


ByVal pClassHelper As esriGeodatabase.IClassHelper, _
ByVal pExtensionProperties As esriSystem.IPropertySet)
Set m_pClassHelper = pClassHelper
m_sUsrName = Environ("USERNAME")
' If object class has been just created then,
' if the default fields are present, use them
If pExtensionProperties Is Nothing Then
Call TryDefaultProperties
Else
' Load extension properties into the module variables
' Turn off errors so that if the property is not present

247

' the module variables will remain as a null string


On Error Resume Next
m_sCreFieldName = _
pExtensionProperties.GetProperty(c_sCreFieldPropName)
m_sModFieldName = _
pExtensionProperties.GetProperty(c_sModFieldPropName)
m_sUsrFieldName = _
pExtensionProperties.GetProperty(c_sUsrFieldPropName)
On Error GoTo 0
End If
The example uses a convention that an empty string for any of the class-level variables indicates that the timestamp
field is not in use (for example, information about username is not being collected).
Extension properties can be used to persist almost any kind of data. For example, you may want to store a symbol,
perhaps for custom rendering. The symbol would be kept in the database with the feature class and could be modified
by the owner of the feature class.
Implementing your own custom interface
You can implement your own interfaces to provide functionality that is particular to your class extension. In the
example, a new non-ESRI interface has been created, ITimestampClassExtension, which provides facilities to manage
which timestamp fields are in use. This interface has been modelled after the IAnnoClassAdmin and
IDimensionClassExtension interfaces. It has various read and write properties to alter the configuration of the
timestamp fields and an UpdateProperties method to apply the changes to the geodatabase.
[Visual Basic 6]

Public Sub ITimestampClassExtension_UpdateProperties()


' Note that user should have an exclusive schema lock
' before calling this method
' Check if the specified fields exist
Call GetFieldPositions
' Make the property set
Dim pPropSet As IPropertySet
Set pPropSet = New esriSystem.PropertySet
pPropSet.SetProperty c_sCreFieldPropName, m_sCreFieldName
pPropSet.SetProperty c_sModFieldPropName, m_sModFieldName
pPropSet.SetProperty c_sUsrFieldPropName, m_sUsrFieldName
' Update the schema
Dim pClassSchemaEdit2 As IClassSchemaEdit2
Set pClassSchemaEdit2 = m_pClassHelper.Class
pClassSchemaEdit2.AlterClassExtensionProperties pPropSet
End Sub
A developer using the class extension could choose to ignore this interface and make changes to the extension
properties directly, but the methods and properties on ITimestampClassExtension make it easier. The developer
should use ISchemaLock to gain an exclusive lock before changing the extension properties, since effectively the
structure of the object class is being changed.
Implementing the IObjectClassEvents interface
IObjectClassEvents lets you catch the creation, modification, and deletion of objects. In the example, this is where the
timestamps are made. The coding is simple, as is shown by the OnCreate event:
[Visual Basic 6]

Private Sub IObjectClassEvents_OnCreate(ByVal obj As esriGeodatabase.IObject)


' Set the creation date and user name
' For Enterprise geodatabases, it is preferable to use the database
' date and username, but for simplicity this sample will just use the
' client OS date and username.
Dim pRow As IRow
Set pRow = obj
If Len(m_sCreFieldName) > 0 Then
pRow.Value(m_lCreField) = Now
End If

248

If Len(m_sUsrFieldName) > 0 Then


pRow.Value(m_lUsrField) = m_sUsrName
End If
End Sub
The methods on IObjectClassEvents will be called by an object class before notifying other related
and external objects.
Note that there is no need to call IRow::Store after making changes to the object within any of these methods.
Indeed, it is Store or Delete that actually causes these events to fire. If, however, you make changes to objects other
than the passed one, you will need to call Store as usual.
Note also that the field positions and username have been precalculated in global variables. If the user were to update
a few thousand rows at once, this could save significant processor time.
For best performance, minimize the amount of processing in IObjectClassEvents.
The editor's undo and redo events do not cause object class events to fire. Any data edits made by the class extension
will be undone or redone automatically, since they form part of the edit operation which is in progress when the object
class event fires.
If you are using the OnCreate, OnDelete or OnChange methods to validate edit operations, do not call
AbortEditOperation on the workspace if your logic indicates that the edit operation is invalid. Instead, raise an
HRESULT error, which will be propagated to the application that is performing the edit on the class. It is the
responsibility of the editing application that receives the error to abort the edit operation. This is especially true when
editing with ArcMap. If you call AbortEditOperation from within the class extension, the ArcMap undo/redo edit stack
will become unsynchronized.
Even if your class extension is concerned with data edits, you should not make any reference to the
editor or any other application objects in your code, since they are not guaranteed to be present
when your object class is opened.
Implementing the IObjectClassDescription and IFeatureClassDescription interfaces
An ObjectClassDescription provides information for ArcCatalog to use when creating a new object class or feature
class. In the New Table or New Feature Class wizards, the ObjectClassDescription results in an entry in the custom
object type combo box.

ArcCatalog's New Feature Class wizard uses feature class descriptions.


For a new table you implement just IObjectClassDescription. For feature classes you implement both

249

IObjectClassDescription and IFeatureClassDescription. These interfaces can be implemented on a class extension or on


a separate coclass.
In the example, you want to let ArcCatalog users create both new timestamped tables and new timestamped feature
classes.
Two new coclasses have been made: on the first, just IObjectClassDescription is implemented; on the second, both
IObjectClassDescription and IFeatureClassDescription.
In this example it would not have been possible to implement both interfaces on the class extension, since what is
returned by the IObjectClassDescription::RequiredFields property is different for new tables than it is for new feature
classes, as feature classes need a geometry field. Moreover, the InstanceCLSID property is different in each case.
[Visual Basic 6]

Private Property Get IObjectClassDescription_RequiredFields() _


As esriGeodatabase.IFields
' Get the required fields for a feature class
Dim pOCDescription As IObjectClassDescription
Set pOCDescription = New esriGeodatabase.FeatureClassDescription
Dim pFieldsEdit As IFieldsEdit
Set pFieldsEdit = pOCDescription.RequiredFields
' Now add the timestamp fields
Call basUtil.AddTimestampFields(pFieldsEdit)
Set IObjectClassDescription_RequiredFields = pFieldsEdit
End Property
The code excerpt above makes use of the standard FeatureClassDescription coclass to provide the default ObjectID
and geometry fields.
IObjectClassDescription::InstanceCLSID should return the UID of a geodatabase Object, Feature, network feature,
custom object or custom feature, as appropriate.
IObjectClassDescription::ClassExtensionCLSID should return the UID of your class extension.
[Visual Basic 6]

Private Property Get IObjectClassDescription_ClassExtensionCLSID() _


As esriSystem.IUID
Dim pUID As esriSystem.IUID
Set pUID = New UID
pUID.Value = "Timestamper.TimestampClassExtension"
Set IObjectClassDescription_ClassExtensionCLSID = pUID
End Property
The class that implements the description interfaces (whether it is the class extension or a separate coclass) must be
registered to the ESRI GeoObject Class Descriptions component category.
Note the ModelName and ModelNameUnique properties on IObjectClassDescription. In fact model names for object
classes must be always be unique within the geodatabase. Because of this fact, they are unsuitable for this kind of
ObjectClassDescription, which can be applied to more than one object class. In the example the model name has been
set to an empty string. It does not matter what value is returned for ModelNameUnique, as this property is now
deprecated. The main use of model names is for UML modelling. Actually, if you create your object classes from a UML
model with the Schema Wizard, ObjectClassDescriptions are unnecessary.
The ModelName property can normally be set to an empty string.
It is not possible to further customize either the New Table or New Feature Class wizard; for example, by adding a new
page to configure your class extension. The timestamp class extension provides default configuration inside its Init
routine, which is guaranteed to run after the object class is created.
[Visual Basic 6]

Private Sub IClassExtension_Init(ByVal pClassHelper As _


esriGeodatabase.IClassHelper, ByVal pExtensionProperties As _
esriSystem.IPropertySet)
Set m_pClassHelper = pClassHelper
' If object class has been just created then,
' if the default fields are present, use them
If pExtensionProperties Is Nothing Then
Call TryDefaultProperties
Else
...

250

Implementing a feature class property page


The timestamper example has a property page that can be used to configure which fields are being used for
timestamping. This property page is a client to the custom interface implemented on the class extension,
ITimestampClassExtension.
The vital code is in the IComPropertyPage_Applies function. This ensures that the property page is only displayed for
object classes that have the timestamper class extension. See Chapter 2, 'Developing Objects', for advice on the
standard implementation of a property page class.
[Visual Basic 6]

Private Function IComPropertyPage_Applies(ByVal Objects As _


esriSystem.ISet) As Boolean
IComPropertyPage_Applies = False
'Apply if object class has a timestamp extension
If (Objects.Count < 1) Then
Exit Function
End If
Objects.Reset
Dim pObject As IUnknown
Set pObject = Objects.Next
Do Until pObject Is Nothing
If (TypeOf pObject Is IObjectClass) Then
Dim pObjectClass As IObjectClass
Set pObjectClass = pObject
If Not pObjectClass.Extension Is Nothing Then
If TypeOf pObjectClass.Extension Is _
ITimestampClassExtension Then
IComPropertyPage_Applies = True
Exit Function
End If
End If
End If
Set pObject = Objects.Next
Loop
End Function
Your property page should be registered to the ESRI Feature Class Property Pages component category and, if
appropriate, to nonspatial data as well such as ESRI Table Property Pages.

251

The timestamping property page appears when you view the properties of the feature class in
ArcCatalog.
See Also About Class Extensions, Managing Class Extensions, PipeValidation Class Extension Example, Class
Extensions and Relationship Classes, and Class Extensions for Annotation and Dimensions.

Class Extensions and Relationship Classes


Consider the example of a polygon feature class of farm fields, which is related via a relationship class to a nonspatial
object class of farms. One farm has many farm fields. You would like to maintain a total-area attribute on the farms
object class, the value being a summation of the field areas within each farm.

On first inspection it would seem that you should make a class extension on the farms table, implementing
IRelatedObjectClassEvents and IRelatedObjectClassEvents2. The RelatedObjectCreated event would add to the
appropriate farm's total area and the RelatedObjectChanged event would adjust the total area (if the change was
spatial). However this solution is inappropriatethere is no RelatedObjectDeleted event, so the total area cannot be
decreased when a polygon is deleted.
The appropriate solution in this case is to extend the polygon feature class (that is, the farm fields) by implementing
IObjectClassEvents. The OnDelete event for a farm field would be used to navigate the relationship through to the
farms table and decrement the total area. The OnCreate and OnChange events would also make the appropriate
changes to the farms table.
In most cases it is simpler and more effective to implement IObjectClassEvents rather than IRelatedObjectClassEvents
and IRelatedObjectClassEvents2. These latter interfaces have various disadvantages:

Performance slows due to an increased number of eventsif the object changed has relationships to many
objects, a RelatedObjectChanged event will be fired on each object.

For example, with a states/counties relationship class, more than 50 counties could receive events for one
change to a state. The event triggering can be reduced by implementing IConfirmSendRelatedObjectEvents on
your class extension.

There is no method of catching the deletion of a related feature (though this may be irrelevant if the relationship
class is composite).

252

The structure of the available events (for example, RelatedObjectSetMoved, RelatedObjectSetRotated) is more
complicated to handle than those for IObjectClassEvents.
IObjectClassEvents usually provides a better solution than IRelatedObjectClassEvents and
IRelatedObjectClassEvents2.

The main use of IRelatedObjectClassEvents and IRelatedObjectClassEvents2 is when implementing a variation of the
composite relationship class behavior, for example, if you want the related object movement and rotation but without
the cascading deletion. It would be difficult to implement this behavior with IObjectClassEvents, because there is no
simple way of picking up the movement vector or rotation amount of the related feature.
Relationship class notification (also referred to as messaging) triggers the events on IRelatedObjectClassEvents and
IRelatedObjectClassEvents2. Setting the notification on a relationship class to anything other than 'None' is only
appropriate in two situations: when you implement IRelatedObjectClassEvents or IRelatedObjectClassEvents2 or for
composite relationship classes.

See Also About Class Extensions, PipeValidation Class Extension, Timestamper Class Extension Example, Managing
Class Extensions, and Class Extensions for Annotation and Dimensions.

Class Extensions for Annotation and Dimensions


You cannot easily implement a class extension on an annotation or dimension feature class. They already use class
extensions, so implementing your own would overwrite the existing extension, leading to problems.

You can, however, aggregate the existing class extension object, though there are some disadvantages to this
approach.
First, Visual Basic does not currently support COM aggregation, so you will need to use Visual C++ to implement the
class extension. Secondly, you should only implement interfaces that are not already implemented by the existing
class extension. For example, when aggregating AnnotationFeatureClassExtension you could implement
IObjectClassValidation but not IRelatedObjectClassEvents. This is because aggregation can't be used to modify the

253

existing behavior of an implemented interfaceif you overrode an interface, the existing behavior would be lost. There
is an additional theoretical possibility that in the future the aggregated object may implement additional interfaces,
possibly resulting in a clash with your class extension.
See Also About Class Extensions, PipeValidation Class Extension, Timestamper Class Extension Example, Class
Extensions and Relationship Classes, and Managing Class Extensions.

About Custom Features


In geodatabases, a custom feature is a feature with additional specialized behavior implemented by the developer.
When implementing a custom feature, you inherit a standard geodatabase feature. You can then choose to implement
new interfaces or override some of the existing standard interfaces.
In this chapter the term custom feature is used to generically cover spatial and nonspatial objects. The more correct
generic term custom object is too easily confused with custom COM objects that extend or customize some other part
of ArcObjects.
Custom features are one of the most advanced geodatabase customizations possible. When deployed, there is really
no difference between a standard geodatabase feature and a developer-supplied custom featurethe ArcGIS
framework treats them in exactly the same way. In fact, many of the geodatabase features were implemented
internally as custom features, for example, annotation features and dimension features. Clearly, the custom feature
mechanism offers a massive capability to extend the geodatabase model. However, with this power comes
responsibility to achieve a robust and efficient implementation, so that ArcGIS will work correctly.
Custom features are one of the most advanced geodatabase customizations possible. In the vast
majority of cases you will not find it necessary to develop custom features.
In the vast majority of cases, you will not find it necessary to develop custom features. As you will see, they can only
be implemented in development environments that support COM aggregation; you can develop custom features in
Visual C++, but not in Visual Basic 6. ESRI has provided facilities so that nearly all geodatabase customizations can be
implemented in class extensions rather than custom features. There is also the alternative of satisfying behavior
requirements by customizing the application, for example, with a tool or an editor extension.
Later in this section, the reasons why you should or shouldn't implement a custom feature will be discussed in detail.
First, a simple example is presented of a custom Tree feature.
See Also TreeFeature Custom Feature Example, Custom Features versus Other Solutions, Making a Class Extension
With Your Custom Feature, and Managing Custom Features.

Tree Custom Feature Example


Object Model Diagram

Example Code Click here.


Description This feature is a subclass of a standard geodatabase Feature. It adds an interface with functions specific
to trees.
DesignCOM aggregation of an esriGeodatabase Feature.

254

License required ArcEditor


Libraries Geodatabase, Geometry, and System
Languages Visual C++
Categories ESRI GeoObjects
Interfaces ITreeFeature
How to use
1.

Open and build the project Tree.dsp to register the DLL and register to component categories.

2.

Open ArcMap and add the Trees feature class from the personal geodatabase in the Extending ArcObjects
sample data. This class has been preconfigured to store custom tree features.

3.

In the ArcMap VBA environment, click Tools, then click References, and browse to the example's DLL.

4.

Run the 'TreeFeatureTest' VBA macro from the .bas file that accompanies the example.

Implementing your own interface


You can use custom features to implement your own interface to provide functionality that is specific to your data.
Take for example a point feature class of trees. You might have a requirement to calculate the age of a tree based on
its recorded planting date. If the trees were implemented as custom features you could define a new interface,
ITreeFeature perhaps, with an Age property. The alternative is to provide a function located elsewhere that client
developers can call; a good place would be on a feature class extension.

In this example, a tree is a feature with an extra property to return the tree's age.
In the case of a custom feature, its use from a Visual Basic client would look something like this:
[Visual Basic 6]

Dim pTree as ITree


Set pTree = pFeature
age = pTree.Age()
In the case of a class extension, the Visual Basic client would be more like the following:
[Visual Basic 6]

Dim pTreeClassExtension as IClassExtension


Set pTreeClassExtension = pFeature.Class.Extension
age = pTreeClassExtension.GetAge(pTree)
Although the custom feature solution results in more elegant coding, there is no clear benefit apart from the fact that
the custom feature can be developed against in the same way as a standard feature.
Consider the esriCarto DimensionFeature. The functionality of dimensions could probably be produced with class
extensions, but they fit better into the ArcGIS object model as kinds of features, and accordingly, developers can use
them more simply.
Considering the extra development complexity in general, the custom feature approach for adding interfaces is only
recommended when you strongly prefer to have developers use the extra functionality directly on the feature.

Handling aggregation
You may find the ESRI CASE tools useful when designing and implementing custom features. In particular, the Code
Generation Wizard will create an ATL-based Visual C++ project with stubbed out methods for your custom feature.
For more details of the CASE tools, see Building a Geodatabase, and also Geodatabase Modeling with UML.
To implement a custom feature, you must aggregate the existing Feature coclass. Of course, you could implement a
custom nonspatial object in the same way by aggregating the existing Object coclass.

The object to be aggregated is known as the inner object. When your object is created, you cocreate a new instance of
the inner object and keep a reference to its IUnknown interface; this is referred to as the inner unknown, since of

255

course your object, the outer, also has an IUnknown interface.


[Visual C++]

HRESULT CTreeFeature::FinalConstruct()
{
HRESULT hr;
IUnknown *pOuter = GetControllingUnknown();
// Aggregate in ESRI's simple Feature object
hr = CoCreateInstance(CLSID_Feature,
pOuter,
CLSCTX_INPROC_SERVER,
IID_IUnknown,
(void**) &m_pInnerUnk)))
if (FAILED(hr)) return E_FAIL;
The clever part of how aggregation works is in the handling of QueryInterface calls. The outer object, on encountering
a request for an interface that it doesn't implement directly, will forward the request to the inner object.
When subsequently another call to QueryInterface is made, the inner object will forward the request to the outer
object (note that a reference to the outer unknown is given to the inner object when it is created). In this way it
appears to the client as though there is only one object that correctly implements a set of interfaces.
The interfaces are defined as usual in the ATL category map, except for those interfaces that are exposed directly from
the inner object. There is a special macro to handle these interfaces as seen below.
[Visual C++]

BEGIN_COM_MAP(CTreeFeature)
COM_INTERFACE_ENTRY(ITreeFeature)
COM_INTERFACE_ENTRY(ISupportErrorInfo)
COM_INTERFACE_ENTRY_AGGREGATE_BLIND(m_pInnerUnk)
END_COM_MAP()
In the macro above, the word blind indicates that the outer object is giving control to the inner object over which
interfaces are exposed to the client. This means that if the esriGeodatabase Feature coclass implements extra
interfaces in the future, your custom feature will also expose those extra interfaces.
Also, in the header file of your custom feature class, note the following line.
[Visual C++]

DECLARE_GET_CONTROLLING_UNKNOWN()
This macro provides the GetControllingUnknown function that is used in the previously described FinalConstruct code.
GetControllingUnknown guarantees to return the outermost unknown in a situation where there is nested aggregation.
It is possible that another developer may want to aggregate your object. If you want to allow your object to be
aggregated, it must be written with that in mind.
It is possible that your custom feature may be aggregated by other developers.
Fortunately, ATL makes this easyyou just choose to support aggregation on the ATL Object Wizard when creating
your custom feature.

When developing a custom feature, you should be aware of an issue related to the inner and outer unknowns. Note
that in the example, for convenience, a reference is kept to the IFeature interface on the inner object (since the
example functionality is so simple, it isn't really necessary to keep this pointer, except for demonstrating this issue).
[Visual C++]

hr = m_pInnerUnk->QueryInterface(IID_IFeature,(void**)&m_pFeature)
if (FAILED(hr)) return E_FAIL;

256

pOuter->Release();
Why is Release called, and moreover, why is it called on the outer unknown? You will note that m_pFeature has been
declared as a normal pointer rather than a smart pointer.
[Visual C++]

IFeature* m_pFeature;
This is to simplify the code for FinalRelease. There is no real need to have the reference count go above one, since the
code is handling the lifetime events of the object being implemented. When m_pFeature is set up, a call to AddRef is
automatically made on the object in question, in this case the inner object. Therefore, a call to Release is required to
decrement the reference count. However the inner object is delegating all its IUnknown calls to the outer object, so
the AddRef actually gets called on the outer unknown. This is why you must make the Release on the outer object.
For more about COM aggregation, refer to the bibliography.

Making your code efficient


Many of the implementation recommendations for class extensions also apply to custom features. For example, avoid
references to applications such as ArcMap. Also, do not use user interface functions.
You should also strive to make your code as efficient as possible, particularly since users might deal with thousands of
your custom features at a time. Note the implementation of get_Age in the example.
[Visual C++]

IFieldsPtr ipFields;
hr = m_pFeature->get_Fields(&ipFields);
if (FAILED(hr)) return E_FAIL;
long lPlantedYearField;
hr = ipFields->FindField(L"YEAR_PLANTED",&lPlantedYearField);
if (FAILED(hr)) return E_FAIL;
if (lPlantedYearField == -1)
{
AtlReportError(CLSID_TreeFeature, _T("Required YEAR_PLANTED field not
found"), IID_ITreeFeature, E_FAIL);
return E_FAIL;
}
The code to find the field will be executed for every feature. In a production environment, it would be better to
additionally implement a class extension that cached the field position, and which the custom feature could call to
avoid extra work.
With custom features, it is important to make your code as efficient as possibleusers may deal with
thousands of your features at a time.
See Also About Custom Features, Custom Features versus Other Solutions, Making a Class Extension With Your
Custom Feature, and Managing Custom Features.

Custom Features Versus Other Solutions


As you can see from the Tree custom feature example, implementing a custom feature presents a significant technical
challenge. Why would you choose the custom feature approach instead of other solutions? Typically the alternatives
will be a class extension or a customization of the application, such as a tool, or an editor extension.
One of the most significant reasons not to implement custom features is that you must use a development
environment that supports aggregation, for example, Visual C++. Unlike class extensions, custom features cannot be
implemented with Visual Basic 6.
Another point to note is that there are no significant limitations of class extensions. All the functionality you would
generally need to extend the behavior of a feature is available at the class extension level.
The following sections explore in more detail at why you should or shouldn't implement a custom feature. The table
below shows a summary of the advantages and disadvantages. The reasons for customizing the geodatabase as
opposed to the application were summarized in the section on class extensions, so they are not included here.
Custom features
Provide near total control over functionality.
Your custom feature can be used by developers in the same
Advantages way as ESRI geodatabase features.
Code is more object oriented.
The only way of implementing complex junctions.
Disadvantages

Other solutions
Technically easier to implement.
No significant functionality
limitations.
Can be implemented in Visual
Basic 6.

Technically challenging to implement.


Performance can suffer, since code is executed redundantly for
Code is less object oriented.
every feature.
Cannot be implemented in Visual Basic 6.

257

Handling of row and relationship events is less stable than


class extensions.

Solving feature symbology


A common reason to consider custom features is difficult symbology requirements. By implementing IFeatureDraw on
your feature, you can control exactly how the feature is displayed. However, instead of a custom feature, it is normally
preferable to implement a custom renderer.
Instead of solving symbology with a custom feature, it is normally preferable to implement a custom
renderer.
As an example, consider a polygon feature class of buildings that, beyond a certain scale, you would like to display as
points rather than polygons.
For a custom feature, the implementation of IFeatureDraw::Draw in this case is fairly simple, but note that code to get
the current map scale needs to be reexecuted for each feature drawn. With a custom renderer you would only need to
get the map scale just once at the start of the drawing loop. Although in this case the processing time difference is
small, it demonstrates a common issue with custom featuresa lot of redundant recalculation can be required for each
feature. When drawing thousands of features this can result in custom features being significantly slower than custom
renderers.
The custom feature solution is more object oriented and probably more elegant since it does not involve programming
a loop. The custom renderer solution however, gives you complete control over the entire drawing process in one piece
of code rather than implementing a routine that is called repeatedly. For more details about custom renderers, see
Chapter 5, 'Extending the Display'.
Whether you implement a custom renderer or a custom feature, you will find IFeatureClassDraw useful. Custom
renderers can be associated with feature classes in the geodatabase by implementing IFeatureClassDraw on the class
extension. For custom features that implement IFeatureDraw, you should set the
IFeatureClassDraw::DoesCustomDrawing property to true. For both custom features and custom renderers,
RequiredFieldsForDraw should be used to define the fields that need to be fetched for display.
Another relative disadvantage of the custom feature approach to symbology is that the symbol in the ArcMap table of
contents does not reflect the display. You can work around this by implementing IFeatureClassDraw to provide a
default unchangeable renderer (set ExclusiveCustomRenderer to true) that also implements ILegendInfo to control the
table of contents. You should also make a renderer property page to show the symbology on the layers property page.
Note that it is possible to mix custom renderers with custom features, if the renderer calls IFeatureDraw::Draw to
display the feature rather than using the display to draw the shape directly. This can be powerful if the custom feature
is limited to one aspect of the rendering and you require different options for the remainder.

Handling data edit events


Custom features can implement IRowEvents, IRelatedObjectEvents and IFeatureEvents to handle edit events. The
following table shows the equivalence between the former two interfaces and those on class extensions, which provide
equivalent functionality.
Custom feature interfaces

Class extension interfaces

IRowEvents

IObjectClassEvents and IObjectClassValidation

IRelatedObjectEvents

IRelatedObjectClassEvents and IRelatedObjectClassEvents2

In general, it is preferable to use class extensions to handle these geodatabase events. A disadvantage of IRowEvents
and IRelatedObjectEvents is that the events might occur before row state is fully determined, in other words, the
events are not triggered at the end of all possible behavior execution. The comparable class extension events,
however, are triggered last so they are more stable for this type of customization. This is especially important for rows
that have related objects and also for network features.
It is normally better to use class extensions to handle geodatabase events.
IFeatureEvents provides events that are related to geometry changes. It does not have a class extension equivalent,
but you should not normally implement it with custom features. The OnSplit event is not generally useful, since it does
not provide access to the two new features (the normal way to handle split and merge policies is through Domain
objects). The OnMerge event is currently reserved by ESRI for future use; it is currently not triggered by ArcGIS. The
InitShape event is currently only triggered when a complex junction is added to a geometric network. Another similar
interface, INetworkFeatureEvents, is currently reserved by ESRI, its methods are not consistently triggered by ArcGIS.
Note that the IRowChanges interface is commonly consumed by custom feature (and class extension) developers;
however, it is never reimplemented.

Overriding standard interfaces


Custom features give you almost total control in a way that class extensions do not. This is particularly apparent with
the capability of using containment to override the standard ESRI interfaces. COM containment (also known as
delegation) cannot be used to implement an entire custom feature since there are some interfaces on the Feature
coclass that cannot be contained, as they are internal to ArcGIS and hidden.
However, after aggregating a feature, you can contain the interfaces you want to customize. The individual methods
on the contained interfaces can then either be implemented in the customized class or the method call can be passed
on to the appropriate method on the contained interface.

258

259

As an example, consider the situation of a feature being rotated by an ArcMap user.


You would like to intercept this event and adjust some attribute of the feature
according to the amount of rotation. With a class extension you could place code in
IObjectClassEvents::OnChange, but it would be hard to determine the amount of
rotation that had taken place. With a custom feature you could override IFeatureEdit
and place your custom code in the RotateSet method before delegating the call to
the inner aggregated object.
You can use COM containment to override the standard interfaces of a
feature. This kind of customization is not recommended in general.
It should be noted that this kind of customization is not recommended in general, for
several reasons. First, nearly all scenarios can be handled by class extensionsthe
rotation example is slightly contrived and is one of the few exceptions. Second, you
can also control functionality by customizing the application. As an example of this,
consider the ArcMap Rotate tool, which handles the special case of point features
whose symbols are rendered according to an angle attribute value of the feature.
This attribute value is automatically updated when the feature is rotated with the
tool. In this case it is the application object (the tool), rather than the database object (the feature), that is handling
the special circumstance. Third, the custom feature solution is probably the least stable location for the customization.
As has already been mentioned, your custom code is executed before the row state is fully determined; moreover,
your own code may trigger further geodatabase behavior, possibly leading to complex scenarios.
There are no theoretical restrictions to you overriding some of the most basic feature interfaces such as IRow;
however, this is not recommendedif you did this, your feature class may not integrate correctly with the rest of
ArcGIS.

Other reasons to use custom features


Complex junctions can only be implemented as custom features. However, there is a lot of overhead (for example,
building the network and complexities in coding) that makes implementing complex junctions prohibitively expensive.
By implementing IFeatureSnap, it is possible to provide custom snapping functionality for a feature class. This facility
was designed specifically for connection points to complex junctions. Usually it is easier to implement a normal snap
agent configured to use just your feature class.
Custom features may be a suitable solution if you want to persist objects with your features in the database. As an
example, annotation features persist text elements. You should be aware that persisting objects in this way could
dramatically increase the size of each row stored in the database, with severe performance results. Annotation feature
classes work around this problem with the SymbolCollection object.
See Also About Custom Features, Tree Custom Feature Example, Making a Class Extension With Your Custom Feature,
and Managing Custom Features.

Making a Class Extension with your Custom Feature


If you make a custom feature, you will nearly always make a class extension to go with it. In particular, implementing
your own interface on the class extension is a good way to integrate your custom feature with class-level events, such
as the feature class being opened.
You will normally implement a class extension to go with your custom feature.
There are some interfaces that can be implemented on class extensions that are particularly relevant to custom
features. Although they are usually not required, it is worth knowing what these interfaces do.
IFeatureClassCreation is used by the Editor object to determine if a feature can be created with a single mouse click.
For example, you may have a polygon feature class but desire to create standard-shaped features from single digitized
points. It requires additional logic in the custom feature to create the correct polygon geometry from the point.
IPersistCustomFeaturesExtension is used to write out information about your custom features to a stream, so they can
be persisted outside the database. ArcMap checks for this interface if the copy and paste tools are used to place
features on the clipboard. Consider the example of annotation (one of ESRI's standard custom features); the text
element object that belongs with the annotation feature needs to be saved to the clipboard when the Copy tool is
used.
See Also About Custom Features, Tree Custom Feature Example, Custom Features Versus Other Solutions, and
Managing Custom Features.

Managing Custom Features


In a similar way to feature class extensions, the COM class representing the custom feature is stored in a DLL, with the
feature class being bound to the DLL via a CLSID, which is recorded in the geodatabase and the registry of the client
machine.
Custom features are managed in a similar way to class extensions.
Once you have finished your custom feature code, there are two steps to deploy it.
1.

You must register the DLL on each client system that will access the feature class of the custom features. You
should also ensure that the COM class of custom features is registered to the ESRI GeoObjects component
category. If you have correctly implemented an ATL category map, this will be done automatically when the DLL
is registered.

260

2.

You must configure the instance CLSID on the feature class that holds the custom features. The instance CLSID
is the GUID of the COM class of custom features. This GUID can be found in your IDL or from the registry. How
you set the CLSID on the feature class depends on whether you are about to create a feature class or alter an
existing one.

If you are creating a new feature class, there are three options for setting the instance CLSID:

Use a FeatureClassDescription object to automate the creation of the feature class in ArcCatalog. Description
objects were discussed in the section on class extensions. You should return the appropriate GUID for the
IObjectClassDescription::InstanceCLSID property.

Call IFeatureWorkspace::CreateFeatureClass, setting up the CLSID parameter with the appropriate GUID.

If you have created a UML model using CASE tools, use the Schema Wizard in ArcCatalog to create your feature
class.

To configure an existing feature class to have custom features, call IClassSchemaEdit::AlterInstanceCLSID on the
feature class. You should first obtain a schema lock to ensure no one else is using the feature class. The required code
is similar to the IClassSchemaEdit::AlterClassExtensionCLSID example in the section on class extensions. Note that
this code only needs to be run once, since it configures the database rather than any aspect of the client. If the feature
class cannot be opened, as may be the case with a previously failed customization, call
IFeatureWorkspaceSchemaEdit::AlterInstanceCLSID.
Note that you can check the instance CLSID of a feature class with IClass::CLSID, which effectively is the get property
equivalent to the put of IClassSchemaEdit::AlterInstanceCLSID. Those feature classes without custom features will
return the appropriate GUID, normally that of a geodatabase Feature.
See Also About Custom Features, Tree Custom Feature Example, Custom Features Versus Other Solutions, and Making
a Class Extension With Your Custom Feature.

About Plug-in Data Sources


ArcGIS deals with several read-only data sources such as StreetMap, CAD, and the SDC format used by RouteMap
IMS.
The method by which ArcGIS handles some of these data sources has been exposed to developers. You can provide
ArcGIS support for your own data formats by implementing a plug-in data source.
A plug-in data source integrates a new data format completely into ArcGIS, albeit in a read-only manner. You can
browse, preview, and manage the data in ArcCatalog. You can select, render, query, label, and join the data in
ArcMap. You can also program with the data source using normal geodatabase interfaces such as IWorkspace and
IFeatureClass.

A plug-in data source integrates a new data format completely into ArcGIS, albeit in a read-only
manner.
There are some limitations: only tables, feature classes, and feature datasets are supported; you cannot integrate
plug-in data sources with geodatabase objects such as relationship classes and geometric networks. Also, only simple
feature types (points, lines and polygons) are supportedfor example, you cannot implement plug-in data sources for
annotation or dimension features.
There are two main alternatives to a plug-in data source. The first is to implement a custom layer; the other
alternative is to implement an OGIS-compliant OLE DB provider. Both these solutions have their own sections in this
book. The comparison of custom data source implementations table on the following page analyzes the relative merits

261

of these different solutions for custom data.


Types of data source
The plug-in data source mechanism can support many kinds of data. It is useful to categorize them into three types:
file-based, folder-based, and database data sources.
To understand different kinds of data sources, it is important to note the distinction between a dataset and a
workspace. A dataset may be a table, a feature class (table with geometry), or a feature dataset (a collection of
feature classes that share the same spatial reference). A workspace is a collection of datasets in the same location.
In a file-based data source, a dataset is a file or group of files, named such that there may be multiple datasets in a
single folder. The folder is considered to be the workspace. If a dataset has several files, the files usually have the
same base name with different extensions. For example, a shapefile is a collection of files that have the same base
name and several extensions, in particular .dbf, .shp, and .shx, as well as optional files like .prj, .sbn, .sbx, and
.shp.xml' (ArcGIS software-generated metadata). So a collection of files named streets.dbf, streets.shp, and
streets.shx are together a single shapefile named 'streets'. Another example of a file-based data source is CAD.
Data sources are categorized into three types: file-based, folder-based, and database data sources.
Shapefiles are an example of a file-based data source.
In a folder-based data source a dataset is a file or group of files, named such that there can be only one dataset in a
folder. The workspace is considered to be the folder that contains the dataset folder. The files usually have the same
names for every dataset, and the name of the folder is the name of the dataset. For example, a coverage consists of a
folder whose name is the name of the dataset. This folder contains files such as aat.adf, arc.adf, arx.adf, bnd.adf,
tic.adf, and others. The folder that contains this folder may have many coverages and has a folder called 'info' that has
information about all coverages in that workspace. A coverage is also an example of a feature dataset. Another
example of a folder-based data source is VPF.
Coverages are an example of a folder-based data source.
In a database data source, a workspace is a file or group of files or is not a file at all, but is specified by a workspace
string only. It is generally possible to have several of these workspaces in a single folder. For example, with an Access
personal geodatabase, a workspace is a single file with a .mdb extension whose base name is the name of the
database. The database can contain multiple datasets. ArcSDE workspaces have no necessary files, though they may
be represented in the file system as a connection file that has a connection string. The connection string specifies how
to access the data over TCP/IP.
A personal geodatabase is an example of a database data source.
This table compares the merits of different solutions for integrating custom data sources with ArcGIS.
Advantages

Disadvantages

Custom
layers

Fairly simple to get a basic implementation


working.
Performance can be optimized as there are
few restrictions on implementation details.
Can implement in VB6, VC++, VB.NET, and
C#.
Data can remain encryptedthe end user
can be limited to GUI access only.

Cannot be used to represent the data in ArcCatalog


(though additional ArcCatalog customizations could be
deployed in parallel).
Read-only (though custom tools for editing could be
deployed in parallel).
Since there are so many interfaces to implement, it can
be difficult to make your layer as fully functional as a
standard FeatureLayer unless you use aggregation.
Not all ArcGIS tools will work with a custom layer unless
you implement all the interfaces that the FeatureLayer
class does; therefore, you generally need to build
custom tools to work with your layer.
Some relevant interfaces cannot be implemented with
VB6, for example, IGeoFeatureLayer.

Plug-in
data
sources

Solution is integrated into ArcGIS (ArcMap,


ArcCatalog, and so on).
Other developers can program against the
data source with normal geodatabase
ArcObjects.
Can implement in VB6, VC++, VB.NET, and
C#.

Read-only.
Significant amount of development required.

Read-writable (if implemented that way).


Generic, open solutionworks in nonArcGIS applications.

Significant amount of development required. More to


make it writable.
Only VC++ is realistic for implementation.
2D data only.

OGIScompliant
OLE DB
providers

See Also Simple Point Plug-In Data Source Example and Other Plug-In Data Source Topics.

262

SimplePoint Plug-In Data Source Example

Object Model Diagram


Example Code Click here.
Description This project implements a plug-in data source for the SimplePoint data format to provide direct read-only
ArcGIS support for the format.
Design Required classes for a plug-in data source
License required ArcGIS Engine, ArcReader, ArcView or above.
Libraries Geodatabase, Geometry, System
Languages Visual Basic, Visual C++
Categories ESRI Plugin Workspace Factory Helpers, ESRI Workspace Factories, and ESRI Gx Enabled Workspace
Factories
Interfaces IPlugInWorkspaceFactoryHelper, IPlugInWorkspaceHelper, IPlugInDatasetHelper, IPlugInDatasetInfo,
IPlugInCursorHelper, and IPlugInFastQueryValues
How to use
1.

If using VB, edit the batch file called '_INSTALL.bat' to make sure it references your ArcGIS install folder.
Run '_INSTALL.bat' to make the appropriate registry entries.
If using VC++, open and build the project SimplePointVC.dsp to register the DLL and to register to
component categories.

2.

In ArcCatalog, browse to the Towns.spt file supplied with the sample. Try previewing the dataset using the
zoom and identify tools. You should also be able to use the Towns dataset in ArcMap.

The case for a simple point plug-in data source


Imagine that you have a regular supply of text files containing geographic locations, but the data in the files has an
unusual format. You would like to use ArcGIS with this data, but you do not want to convert data every time a new file
is received. In short, you would like ArcGIS to work with this data directly, just like it does with other supported data
formats. This can be done by implementing a plug-in data source.
The SimplePoint plug-in data source provides direct ArcGIS support for an unusual data format.
The data you will work with in this example follows a simple format. An ASCII text file contains data for each new
point on a new line. The first six characters are the x-coordinate, the next six characters contain the y-coordinate, and
the trailing characters contain an attribute value.

263

Creating a plug-in data source


To make a plug-in data source, you must implement four required classes:

A plug-in workspace factory helper


A plug-in workspace helper
A plug-in dataset helper
A plug-in cursor helper

As a developer you will typically name these classes with a prefix corresponding to your
data sourcein the VB6 example they are called SPTWorkspaceFactoryHelper,
SPTWorkspaceHelper, SPTDatasetHelper and SPTCursorHelper. In some documentation you
will sometimes see these classes referred to generically with the prefix 'PlugIn', for
example, a PlugInWorkspaceHelper.
As well as the four required classes, a plug-in data source can have an optional plug-in
extension class and possibly several plug-in native type classes. These are not implemented
in the example, but will be discussed later.
With each class there are one or more interfaces you need to implement. For detailed help
on individual interface members, see the ArcGIS Developer Help.

Implementing a plug-in workspace factory helper


A workspace factory helper class must implement the IPlugInWorkspaceFactoryHelper interface. This helper class
works in conjunction with the existing ArcGIS PlugInWorkspaceFactory coclass.
The PlugInWorkspaceFactory class implements IWorkspaceFactory and uses the plug-in workspace factory helper to
get information about the data source and to browse for workspacestogether they act as a workspace factory for the
data source.
The implementation of the workspace factory helper in the Visual Basic 6 example differs from that in the Visual C++
example. The crucial part of the Visual Basic 6 implementation is what you return for
IPlugInWorkspaceFactoryHelper::WorkspaceFactoryTypeID. Instead of the CLSID of the workspace factory helper, you
should return a CLSID that does not refer to any implementation. It will be used as an alias for the workspace factory
of the data source that is created by the PlugInWorkspaceFactory.
You can generate the CLSID using Guidgen or an equivalent tool.
[Visual Basic 6]

Private Property Get IPlugInWorkspaceFactoryHelper_WorkspaceFactoryTypeID() As IUID


Dim pUID As esriSystem.IUID
Set pUID = New UID
pUID.Value = "{6322F361-E3F0-11d5-8A7A-00104BB6FCCB}"
Set IPlugInWorkspaceFactoryHelper_WorkspaceFactoryTypeID = pUID
End Property
A Visual Basic 6 plug-in workspace factory helper should be registered in the component category ESRI Plugin
Workspace Factory Helpers. You should then reregister PlugInWorkspaceFactory.dll (this file is found in your ArcGIS
installation bin folder). This reregistration will register the CLSID you returned in WorkspaceFactoryTypeID in the ESRI
Workspace Factories and ESRI Gx Enabled Workspace Factories categories.
Note that when it comes to uninstalling, simply unregistering the Visual Basic 6 project DLL would orphan the registry
entries for the alias CLSID. The correct procedure for uninstallation is to unregister PlugInWorkspaceFactory.dll,
unregister the Visual Basic 6 DLL, then reregister PlugInWorkspaceFactory.dll. This can be seen in the example's
uninstallation batch file.
If you implement a plug-in workspace factory helper with C++, or another language that supports class aggregation, it
should aggregate an instance of the existing geodatabase PlugInWorkspaceFactory coclass and register in the ESRI
Workspace Factories and ESRI Gx Enabled Workspace Factories component categories. You must implement the
workspace factory helper as a singleton object. The need for the singleton is a consequence of the following rule for
data sources: datasets must be pointer comparable. That is, there can only be one dataset object for a dataset in each
process thread. To ensure this, there must be only one workspace object for each workspace, and thus only one
workspace factory that creates workspaces.
The architecture of a plug-in workspace factory helper implemented in Visual Basic 6 is significantly
different from one implemented in Visual C++.
In addition to implementing the workspace factory as a singleton, you must maintain a cache of the plug-in
workspaces that have been opened, and in each workspace object, a cache of the open datasets. These caches are
used to avoid creating a second dataset object, when one already exists for that dataset in the process. Note that
singleton objects cannot be implemented in Visual Basic 6. The ArcGIS framework works around this problem by the
previously described registration procedure, which enables ArcGIS to create the singleton and maintain the object
caches for Visual Basic 6 implementations.
Whichever way the workspace factory helper is implemented, you could choose not to register to ESRI Gx Enabled
Workspace Factories. This component category instructs ArcCatalog to create standard user-interface objects for the

264

data source. If you don't register to this category, you will need to implement custom ArcCatalog objects for the data
source to be displayed in ArcCatalog. There is more information about why you would adopt this approach later in this
section.
Returning to the example, the remaining implementation of IPlugInWorkspaceFactoryHelper is mainly straightforward.
The hardest member to implement is often GetWorkspaceString. The workspace string is used as a lightweight
representation of the workspace.
Your plug-in is the sole consumer (IsWorkspace and OpenWorkspace) of the strings, so their content is up to you. For
many data sources, including the example, the path to the workspace is chosen as the workspace string. Another thing
to note about GetWorkspaceString is the FileNames parameter. This parameter may be null, in which case you should
call IsWorkspace to determine if the directory is a workspace of your type. If the parameter is not null, you should
examine the files in FileNames to determine if the workspace is of your type. You also need to remove any files from
the array that belong to your data source. This behavior is comparable to that of
IWorkspaceFactory::GetWorkspaceName.
The DataSourceName property is simple to implementjust return a string representing the data source. The example
returns "SimplePoint". This is the only text string that should not be localized. You should localize the other strings (for
example, by using a resource file) if your plug-in data source could be used in different countries. For simplicity, the
example does not localize its strings.
The OpenWorkspace method creates an instance of the next class you must implement, the plug-in workspace helper.
You need a way of initializing the workspace helper with the location of the data. The example does this by defining a
new interface on the workspace helper, ISPTWorkspaceHelper, which provides a WorkspacePath property so that the
location of the workspace can be passed.
[Visual Basic 6]

Private Function IPlugInWorkspaceFactoryHelper_OpenWorkspace( _


ByVal wksString As String) As IPlugInWorkspaceHelper
Dim pFSO As Object
Set pFSO = CreateObject("Scripting.FileSystemObject")
If Not pFSO.FolderExists(wksString) Then
Err.Raise E_FAIL, "OpenWorkspace", "Workspace string invalid: " & wksString
Exit Function
End If
' Create the workspace helper object
Dim pSPTWorkspaceHelper As ISPTWorkspaceHelper
Set pSPTWorkspaceHelper = New SPTWorkspaceHelper
pSPTWorkspaceHelper.WorkspacePath = wksString
Set IPlugInWorkspaceFactoryHelper_OpenWorkspace = _
pSPTWorkspaceHelper ' Inline QI to IPlugInWorkspaceHelper
End Function
For convenience, the new interface is defined in the Visual Basic project rather than with IDL. As described in 'Creating
Type Libraries using IDL' in Chapter 2, interfaces defined in this way cannot be easily called from Visual C++ clients.
However, in this case, there is no problem as the only consumer of the interface is the Visual Basic project.
Plug-in workspace factories may also implement the optional interface IPlugInCreateWorkspace to support creation of
workspaces for a plug-in data source. See Implementing copy, rename and delete for plug-in data sources for more
details.
Plug-in workspace factories may also implement the optional interface IWorkspaceFactoryFileExtensions to help
improve ArcCatalog efficiency. See Improving browse performance in ArcCatalog for plug-in data sources for more
details.

Implementing a plug-in workspace helper

A plug-in workspace helper represents a single workspace for datasets of your data source type. The class does not
need to be publicly cocreatable, as the plug-in workspace factory helper is responsible for creating it in its
OpenWorkspace method.
The class must implement IPlugInWorkspaceHelper; this interface allows browsing of datasets. The most noteworthy
member is OpenDataset, which creates and initializes an instance of a plug-in dataset helper.
[Visual Basic 6]

Private Function IPlugInWorkspaceHelper_OpenDataset(ByVal localName _


As String) As IPlugInDatasetHelper
' Check if the dataset is valid

265

Dim pFSO As Object


Set pFSO = CreateObject("Scripting.FileSystemObject")
If Not pFSO.FileExists(m_sWorkspacePath & "\" & localName & _
g_sFileExtension) Then
Set IPlugInWorkspaceHelper_OpenDataset = Nothing
Err.Raise E_FAIL, , "Dataset does not exist: " & localName
Exit Function
End If
' Create the dataset helper object
Dim pSPTDataset As ISPTDatasetHelper
Set pSPTDataset = New SPTDatasetHelper
pSPTDataset.DatasetName = localName
pSPTDataset.WorkspacePath = m_sWorkspacePath
Set IPlugInWorkspaceHelper_OpenDataset = pSPTDataset ' Inline QI
End Function
If the SupportsSQLSyntax property of IPlugInWorkspaceFactoryHelper returns true, your plug-in workspace helper
should implement the ISQLSyntax interface. In this case, the workspace object will delegate calls to its ISQLSyntax to
the interface on this class. The ArcGIS framework will pass where clauses to the IPlugInDatasetHelper::FetchAll and
FetchByEnvelope, and the cursors returned by these functions should contain only rows that match the where clause.
If SupportsSQLSyntax returns false, the ArcGIS framework won't pass where clauses, but will handle them with postquery filtering. The advantage of implementing support for where clauses is that you may be able to process queries
on large datasets more efficiently than a post-query filter. The disadvantage is the extra implementation code
required. The example returns false for SupportsSQLSyntax and so leaves handling of where clauses to the ArcGIS
framework.
A plug-in workspace helper may implement IPlugInMetadata or IPlugInMetadataPath to support metadata. Implement
IPlugInMetadata if your data source has its own metadata engine; this interface allows metadata to be set and
retrieved as property sets. Otherwise, implement IPlugInMetadataPath; it allows the plug-in to specify a metadata file
for each dataset. ArcGIS will then use these files for storing metadata. You should implement one of these interfaces
for successful operation of the Export Data command in ArcMap. This command uses the FeatureDataConverter object
which relies on metadata capabilities of data sources.
A plug-in workspace helper may also implement the optional interface IPlugInWorkspaceHelper2. See Implementing
attribute indexes for plug-in data sources for more details.
A plug-in workspace helper may also implement the optional interface IPlugInLicense. See Implementing license
handling for plug-in data sources for more details.

Implementing a plug-in dataset helper


A plug-in dataset helper class must implement the IPlugInDatasetInfo and
IPlugInDatasetHelper interfaces. It does not need to be publicly cocreatable, as a
plug-in workspace helper is responsible for creating it.
IPlugInDatasetInfo provides information about the dataset so that the user interface
can represent it. For example, ArcCatalog uses this interface to display an icon for
the dataset. To enable fast browsing, it is important that the class have a low
creation overhead. In the example, the SPTDatasetHelper class can be created and
all the information for IPlugInDatasetInfo derived without opening the data file.
IPlugInDatasetHelper provides more information about the dataset and methods to access the data. If the dataset is a
feature dataset (that is, it contains feature classes), all of the feature classes are accessed via a single instance of this
class. Many of the interface members have a ClassIndex parameter that determines which feature class is being
referred to.
IPlugInDatasetHelper::Fields defines the columns of the dataset. For the SimplePoint data source, all datasets have
just three fields: Object ID, Shape, and a single attribute field, which in the example is arbitrarily named 'Column1'.
When implementing Fields you must define the spatial reference of your dataset. In the example, for simplicity, an
UnknownCoordinateSystem is chosen. If your spatial reference is a geographic coordinate system, you should put the
extent of the dataset into the IGeographicCoordinateSystem2::ExtentHint property before setting the domain of the
spatial reference. Setting the domain first can cause problems with projections and export.
[Visual Basic 6]

Private Property Get IPlugInDatasetHelper_Fields(ByVal ClassIndex As Long) As


esriGeodatabase.IFields
' Start off with a default feature class fields collection
Dim pObjectClassDescription As IObjectClassDescription
Set pObjectClassDescription = New FeatureClassDescription
Dim pFields As esriGeodatabase.IFields

266

Dim pFieldsEdit As esriGeodatabase.IFieldsEdit


Set pFields = pObjectClassDescription.RequiredFields
Set pFieldsEdit = pFields
Dim pField As esriGeodatabase.IField
Dim pFieldEdit As esriGeodatabase.IFieldEdit
' We will have: a shape field name of "shape", an
' UnknownCoordinateSystem. Just need to change geometry type to Point
Dim i As Integer
For i = 0 To pFields.FieldCount - 1
Set pField = pFields.Field(i)
If pField.Type = esriGeodatabase.esriFieldType.esriFieldTypeGeometry Then
Dim pGeomDefEdit As esriGeodatabase.IGeometryDefEdit
Set pGeomDefEdit = pField.GeometryDef
pGeomDefEdit.GeometryType = esriGeometry.esriGeometryType.esriGeometryPoint
Exit For
End If
Next i
' Add the extra text field
Set pFieldEdit = New esriGeodatabase.Field
With pFieldEdit
.Length = 1
.Name = "Column1"
.Type = esriGeodatabase.esriFieldType.esriFieldTypeString
End With
pFieldsEdit.AddField pFieldEdit
Set IPlugInDatasetHelper_Fields = pFieldsEdit
End Property
All data sources must include an Object ID field. If your data does not have a suitable unique integer field, then you
will need to generate a value on the fly. As will be seen later, the example uses the current line number in the text file
as the Object ID. Another data source without explicit Object IDs is the shapefile format. In a similar way the ArcGIS
framework generates a suitable unique integer automatically for each feature in a shapefile.
There are three similar members of IPlugInDatasetHelper that all open a cursor on the dataset: FetchAll,
FetchByEnvelope, and FetchByID. In the example, all these methods cocreate a new plug-in cursor helper and initialize
it with various parameters that will control the operation of the cursor. Here is the implementation of
FetchByEnvelope.
[Visual Basic 6]

Private Function IPlugInDatasetHelper_FetchByEnvelope( _


ByVal ClassIndex As Long, ByVal env As esriGeometry.IEnvelope, _
ByVal strictSearch As Boolean, ByVal WhereClause As String, _
ByVal FieldMap As Variant) As esriGeodatabase.IPlugInCursorHelper
Dim pSPTCursorHelper As ISPTCursorHelper
Set pSPTCursorHelper = New SPTCursorHelper
pSPTCursorHelper.FieldMap = FieldMap
Set pSPTCursorHelper.QueryEnvelope = env
pSPTCursorHelper.FilePath = m_sWorkspacePath & "\" & m_sDatasetName & g_sFileExtension
' Inline QI
Set IPlugInDatasetHelper_FetchByEnvelope = pSPTCursorHelper
End Function
An ISPTCursorHelper interface has been defined on the SPTCursorHelper class to pass parameters. In the above code,
three parameters have been set: the field map will control which attribute values are fetched by the cursor, the query
envelope will determine which rows are fetched by the cursor, and the filepath tells the cursor where the data is.
The example is able to ignore some of the FetchByEnvelope parameters as ClassIndex applies only to feature classes
within a feature dataset and WhereClause applies only to those data sources supporting ISQLSyntax; strictSearch can
be ignored since the example does not use a spatial index to perform its queries, and so always returns a cursor of
features that strictly fall within the envelope.
There are other equally valid ways of implementing FetchByEnvelope, FetchById, and FetchAll; with your data source it
may be more appropriate to create the cursor helper, then use a postprocess to filter the rows to be returned.
There is one more member of IPlugInDatasetHelper that is worth mentioning. The Bounds property returns the
geographic extent of the dataset. Many data sources have the extent recorded in a header file, in which case
implementing Bounds is easy. However, in the example, a cursor on the entire dataset must be opened and a

267

minimum-bounding rectangle gradually built. The implementation makes use of IPlugInCursorHelper. Note that it
would be quite unusual for another developer to consume the plug-in interfaces in this way, since once your data
source is implemented, the normal geodatabase interfaces will work with it (albeit in a read-only manner). Another
point to note about the Bounds property is that you must create a new envelope or clone a cached envelope. You can
run into problems with projections if your class caches the envelope and passes out pointers to the cached envelope.
A plug-in dataset helper should implement IPlugInFileSystemDataset if the data source is file-based and multiple files
make up a dataset. Single-file and folder-based data sources do not need to implement this interface.
A plug-in dataset helper should implement IPlugInRowCount if the RowCountIsCalculated property of the workspace
helper returns false. Otherwise, this interface should not be implemented. If you implement this interface, make sure
it operates quickly. It should be faster than just opening a cursor on the entire dataset and counting.
A plug-in dataset helper may also implement the optional interfaces IPlugInFileOperations and
IPlugInFileOperationsClass. See Implementing copy, rename, and delete for plug-in data sources for more details.
A plug-in dataset helper may also implement the optional interfaces IPlugInIndexInfo and IPlugInIndexManager. See
Implementing attribute indexes for plug-in data sources for more details.
A plug-in dataset helper may also implement the optional interface IPlugInLicense. See Implementing license handling
for plug-in data sources for more details.

Implementing a plug-in cursor helper

The plug-in cursor helper deals with the raw data and is normally the class for which you will write the most code. The
cursor helper represents the results of a query on the dataset. The class must implement the IPlugInCursorHelper
interface, but does not need to be publicly cocreatable, as the plug-in dataset helper is responsible for creating it.
NextRecord advances the cursor position. In the example, a new line of text is read from the file and stored in a string.
As was described in the previous section, the dataset helper defines the way the cursor will operate; this is reflected in
the example's implementation of NextRecord. If a record is being fetched by object ID, the cursor is advanced to that
record. If a query envelope is specified, the cursor is moved on to the next record with a geometry that falls within the
envelope.
[Visual Basic 6]

Private Sub IPlugInCursorHelper_NextRecord()


' We will take the line number in the file to be the OID of the feature
' If you are searching by OID, skip to the correct line
If m_lOID <> -1 Then
Do Until m_lOID = m_pStream.Line
If m_pStream.AtEndOfStream Then
m_sCurrentRow = ""
Err.Raise E_FAIL
Else
m_pStream.SkipLine
End If
Loop
End If
' Read the line
If m_pStream.AtEndOfStream Then
m_sCurrentRow = ""
Err.Raise E_FAIL
Else
m_sCurrentRow = m_pStream.ReadLine
End If
' If you are finding by envelope, check the current record. If not in
' the envelope, make a recursive call to move on to the next record
If Not m_pQueryEnv Is Nothing Then
Call IPlugInCursorHelper_QueryShape(m_pWorkPoint)
Dim pRelOp As IRelationalOperator
Set pRelOp = m_pWorkPoint
If Not pRelOp.Within(m_pQueryEnv) Then
Call IPlugInCursorHelper_NextRecord
End If

268

End If
End Sub
A Visual Basic implementation of NextRecord must raise an error if there are no more rows to fetch.
One thing to note about NextRecord is that, with Visual Basic 6 you must return an error when there are no more
records to fetch.
With Visual C++ or other suitable languages, you should return S_FALSE (this value cannot be raised by Visual Basic
6). To enable debugging of a Visual Basic 6 implementation, it is useful to choose the 'Break on Unhandled Errors'
setting on the General tab of the Options dialog box; this prevents the debugger from stopping whenever an object
passes back an error HRESULT.
QueryShape should return the geometry of the feature. In common with many other ArcObjects methods having a
name beginning with Query, the object to be returned is already instantiated in memory. VB developers in particular
may find it helpful to review the 'Clientside storage members' section in Chapter 2 for more information.
For this PlugInCursorHelper, you only need to set the coordinates of the point feature.
[Visual Basic 6]

Private Sub IPlugInCursorHelper_QueryShape(ByVal pGeometry As IGeometry)


...
' The passed geometry should be pointing to an instantiated object
' we just need to fill in the contents
Dim pPoint As IPoint
Set pPoint = pGeometry
' Parse the X and Y values out of the current row and into the geometry
pPoint.X = CDbl(Left(m_sCurrentRow, 6))
pPoint.Y = CDbl(Mid(m_sCurrentRow, 7, 6))
End Sub
For data sources with complex geometries, you can improve the performance of QueryShape by using a shape buffer.
Use IESRIShape::AttachToESRIShape to attach a shape buffer to the geometry. This buffer should then be reused for
each geometry.
The ESRI white paper, ESRI Shapefile Technical Description, can be referred to for more information on shape buffers,
as shapefiles use the same shape format. You can find the white paper on the ESRI Web site, www.esri.com.
QueryValues returns the attributes of the current record. The field map (specified when the cursor was created)
dictates which attributes to fetch. This is designed to improve performance by reducing the amount of data transfer;
for example, when features are being drawn on the map, it is likely that only a small subset, or even none of the
attribute values, will be required. The return value of QueryValues is interpreted by ArcGIS as the Object ID of the
feature.
[Visual Basic 6]

Private Function IPlugInCursorHelper_QueryValues( _


ByVal Row As esriGeodatabase.IRowBuffer) As Long
...
' First, parse the attribute out of the current row.
' We know there is just one attribute, which is one char wide.
Dim sAtt As String
sAtt = Right(m_sCurrentRow, 1)
Dim pField As IField
Dim pFields As IFields
Set pFields = Row.Fields
...
' For each field, copy its value into the row object. (don't copy
' shape, object ID or where the field map indicates no values required)
' Note, although we know there is only one attribute in the data source,
' this loop has been coded generically in case support needs to be added
' for more attributes
Dim i As Long
For i = 0 To pFields.FieldCount - 1
Set pField = pFields.Field(i)
If (Not pField.Type = esriFieldTypeGeometry) And _
(Not pField.Type = esriFieldTypeOID) And _
(m_vFieldMap(i) <> -1) Then
Row.Value(i) = sAtt
End If

269

Next i
' Return value is taken as the OID.
' Use the line number (stream will currently be pointing at next line)
IPlugInCursorHelper_QueryValues = m_pStream.Line - 1
End Function
Implementing IPlugInFastQueryValues
A plug-in cursor helper implemented in Visual C++ may implement IPlugInFastQueryValues. The only method,
FastQueryValues, should do the same thing as IPlugInCursorHelper::QueryValues, but as it passes open arrays, you
should be able to provide a more efficient implementation. The open arrays prevent FastQueryValues from being
implemented in Visual Basic 6.
Note that it is possible to implement the plug-in cursor helper with C++ and the other required classes with Visual
Basic 6.
See Also About Plug-In Data Sources and Other Plug-In Data Source Topics.

Other Plug-In Data Source Topics


Plug-In Data Source Objects
Below, you can see the general object model for the ArcGIS plug-in data source objects.

Implementing copy, rename, and delete for plug-in data sources


You can integrate your data source with the copy, rename, and delete functionality of ArcGIS including the dragging
and dropping of datasets.

270

To support this there are two optional interfaces that can be implemented on the plug-in dataset helper.
IPlugInFileOperations is used to operate on a feature dataset or a standalone table or feature class.
IPlugInFileOperationsClass operates on classes within a feature dataset. The members of the two interfaces work in
the same way, the difference being that an extra class index parameter is present on the IPlugInFileOperationsClass
members.
IPlugInFileOperations::Rename takes a name and also returns a name. The input name may or may not have a file
extension on it. The output name must be in the form that names are passed to the data source on creation. When
implementing Rename, remember to change any cached representation of the dataset name that you have in your
plug-in dataset helper.
When implementing copy and paste with file- or folder-based data sources, remember that a user may paste a dataset
to a location that is empty of other datasets. To allow this you should implement
IPlugInWorkspaceFactoryHelper::OpenWorkspace to succeed on locations that are empty.
The rest of ArcObjects will also work with your copy/rename/delete implementation. For example, a client of your
plug-in data source could call IDataset::Delete on one of your datasets.
Some developers will programmatically copy data by first calling IWorkspaceFactory::CreateWorkspace to make a
workspace before copying data into it. To support this for your plug-in data source, implement
IPlugInCreateWorkspace on your plug-in workspace factory. An implementation of this interface on a plug-in
workspace factory helper registered by the Visual Basic 6 method will not be called.

Implementing attribute indexes for plug-in data sources


You may support the concept of attribute indexes on your plug-in data source to speed up certain queries. You can
configure your plug-in data source so that the standard ArcCatalog user interface works with your data for displaying
and manipulating attribute indexes.

The Add and Delete buttons are enabled if you implement IPlugInWorkspaceHelper2 on your workspace helper and
return true for CanAddIndex and CanDeleteIndex. In this case you must also return false for

271

IPlugInWorkspaceHelper2::IsReadOnly; this sets the esriWorkspacePropIsReadonly property for the workspace. This
property does not indicate whether your plug-in data source supports read/write, rather that this particular workspace
can be written to. In the case of adding an index, you are changing the schema of your data and will be writing the
fact that the index has been created to the workspace in some way.
To support the listing of indexes in the user interface, implement IPlugInIndexInfo on your plug-in dataset helper. If
there are no indexes, you should return a pointer to an empty Indexes object rather than a null pointer. To support
the addition and deletion of indexes, implement IPlugInIndexManager on your plug-in dataset helper. You must
implement this interface if you return true for CanAddIndex and CanDeleteIndex on IPlugInWorkspaceHelper2.
How you implement the indexes and the handling of queries is up to you; the plug-in interfaces enable integration with
ArcGIS for manipulation of the indexes. This includes the rest of ArcObjects, for example, a client of your plug-in data
source could call IClass::Indexes to get the list of indexes on your class.

Implementing license handling for plug-in data sources


If your plug-in data source will require a license to use, you must implement a plug-in extension class and also
IPlugInLicense on either the plug-in workspace helper or the plug-in dataset helper.
The plug-in extension class must implement IExtension and register itself in the ESRI Mx Extensions (for drawing) and
ESRI Gx Extensions (for browsing) component categories. The class may implement IExtensionConfig, in which case it
will appear in the extensions dialog box. It should also implement IAutoExtension if it automatically enables and
disables.
A plug-in workspace helper should implement IPlugInLicense if enabling the license enables all datasets of the data
source type. If only some datasets are to be enabled, the interface should be implemented on the plug-in dataset
helper. If the interface is implemented on both classes, the implementation on the dataset helper will be used. The
license is checked when feature classes, tables, and cursors from data sources are created.

Enabling ArcCatalog searches with plug-in data sources


The search tools in ArcCatalog enable the user to search for types of datasets. Ideally, a plug-in data source should
include a plug-in native type coclass for each type of dataset supported by the data source. As an example, if a data
source supports both tables and feature classes (like shapefiles), it should have two native type coclasses; one might
be called PlugInTableNativeType and the other called PlugInFeatureClassNativeType.
A plug-in native type class must implement the INativeType interface and register in the ESRI Native Types
component category. Your implementation of IPlugInWorkspaceHelper::NativeType should return the appropriate
native type object for each dataset type.
The native type classes are not essential. However, if you do not implement them, the search tools in ArcCatalog will
not be able to search specifically for datasets of your data source.

Custom context menus and plug-in data sources


You may want to add your own commands to the context menus of your plug-in data
source. For example you could provide a command to export your data to another
nonstandard format.
There are four relevant component categories, but these are shared between all
plug-in data sources:

ESRI GX Read-only Feature Class Context Menu Commands


ESRI GX Read-only Standalone Feature Class Context Menu Commands
ESRI GX Read-only Table Context Menu Commands

272

ESRI GX Read-only Feature Dataset Context Menu Commands

If you add commands to these component categories, they will appear on the context menus of all plug-in data
sources, thus your solution may not integrate with that of a third party.
An alternative approach is to implement your own ArcCatalog objects for your plug-in data source, in particular a
GxObjectFactory and a GxObject. This enables you to provide custom context menus that apply just to datasets of
your type and also custom icons. For this solution, do not register the plug-in workspace factory to ESRI Gx Enabled
Workspace Factories. For more information about how to implement ArcCatalog objects, see Chapter 6, 'Adapting the
Catalog'.

Improving browse performance in ArcCatalog for plug-in data sources


To improve browsing performance and memory usage, ArcCatalog caches information about what file extensions are
claimed by data sources. This information is used to activate data sources only when data for them is found. It is used
to determine what files will be passed to the workspace factory to claim. This speeds up the process of claiming files.
To support this, workspace factories may implement the optional interface IWorkspaceFactoryFileExtensions.
IWorkspaceFactoryFileExtensions has two methods; the first returns activation extensions. The workspace factory will
be loaded when any of these extensions are found. The second method returns relevant extensions; these are all the
extensions that will be passed to the workspace factory to claim. If the interface is not implemented, the activation
and relevant extensions will both be "*", which matches everything. Extensions are listed in a string, separated by the
vertical bar, '|'. As an example, a data source that implemented the shapefile format might return "shp|dbf" for the
activation extensions, and "shp|shx|sbn|dbf|prj|xml" for the relevant extensions. Before testing your implementation,
you may need to delete the existing cache file (GxDBFactCache.dat, normally in <your user profile>\Application
Data\ESRI\ArcCatalog folder) for there to be any changes from your previous implementation.
IWorkspaceFileFactoryExtensions may only be implemented on plug-in workspace factories implemented by
aggregation. An implementation of this interface on a plug-in workspace factory helper registered by the Visual Basic 6
method will not be called.

Programmatically accessing plug-in data sources


One of the advantages of plug-in data sources is that once implemented, they can be accessed by client developers
using the normal geodatabase ArcObjects API. You only need to use the interfaces with names starting 'IPlugIn' when
implementing the data source. One problem you may encounter is when your plug-in workspace factory was
implemented with the Visual Basic 6 registration method rather than the aggregation method. In this case you will not
have any type library information for the workspace factory, so you will have to use late binding to create the object
via its ProgID. You can find the ProgID of your workspace factory by using the Component Category Manager tool to
inspect the ESRI Workspace Factories category. For Visual C++ clients there is no problem since you can just use the
CLSID you returned in IPlugInWorkspaceFactoryHelper::WorkspaceFactoryTypeID to directly cocreate the workspace
factory. The VBA code below adds a SimplePoint dataset as a layer to ArcMap.
[Visual Basic 6]

Public Sub AddPlugInLayer()


Dim pFeatWorkspace As IFeatureWorkspace
Dim pFeatClass As IFeatureClass
Dim pWorkspaceFactory As IWorkspaceFactory
' Following line uses correct ProgID for VB6 implementation, but won't compile because no type lib
info
' Set pWorkspaceFactory = New esriGeoDatabase.SPTWorkspaceFactory
' Following line would work fine for our C++ implementation
' Set pWorkspaceFactory = New SIMPLEPOINTVCLib.SimplePointWorkspaceFactory
Set pWorkspaceFactory = CreateObject("esriGeoDatabase.SPTWorkspaceFactory")
Set pFeatWorkspace = pWorkspaceFactory.OpenFromFile("D:\Data\SimplePoint", 0)
Set pFeatClass = pFeatWorkspace.OpenFeatureClass("Towns")
Dim pFeatLayer As IFeatureLayer
Set pFeatLayer = New FeatureLayer
Set pFeatLayer.FeatureClass = pFeatClass
pFeatLayer.Name = pFeatClass.AliasName
Dim pMxDocument As IMxDocument
Dim pMap As IMap
Set pMxDocument = Application.Document
Set pMap = pMxDocument.FocusMap
pMap.AddLayer pFeatLayer
End Sub
You can also open the workspace with IWorkspaceFactory::Open. In this case supply a property set with a single
property, DATABASE, having the appropriate workspace string. Another way of opening the workspace is to use a
WorkspaceName object; set the WorkspaceFactoryProgID and PathName properties, then call IName::Open.

273

See Also About Plug-In Data Sources and Simple Point Plug-In Data Source Example.

About Workspace Extensions


A workspace extension extends the functionality of a geodatabase in a manner that applies to the whole database
rather than individual datasets. Like class extensions, workspace extensions can only be applied to geodatabases;
though, in general, they are much less important than class extensions as a way of extending geodatabase behavior.
Here are some of the things you can do with a workspace extension:

Filter out datasets that end users should not be able to see or edit. This is a common requirement for data
dictionary tables that are specific to your application.

Implement your own interface on a workspace extension. The extension can be a useful place to cache
geodatabasewide data or behavior.

Handle workspace editing events with IWorkspaceEditEvents. All these events are also available on the Editor
object, but a workspace extension provides a way of listening to edits that might be made without the editor, for
example, edits made programmatically after a call to IWorkspaceEdit::StartEditing.

Handle versioning events with IVersionEvents. This interface can alternatively be used with the editor.

Handle dataset creation and deletion events with IWorkspaceEvents. There is normally an alternative way of
solving these problems by customizing the application. The reasons for using the workspace extension approach
are similar to those for using class extensions as opposed to customizing the applicationsee the table in the
class extensions section earlier in this chapter. Beware, howeverif your workspace extension code fails
unexpectedly, your whole database could be inaccessible rather than just one feature class.
A workspace extension extends the functionality of an entire geodatabase.

See Also ConnectLog Workspace Extension Example and Managing Workspace Extensions.

Connection Log Workspace Extension Example


Object Model Diagram

Example Code Click here.


Description This project provides an enhancement to a geodatabase, which records people connecting to the
geodatabase, maintaining a log table of who connected along with the date and time. The log table is hidden from the
ArcGIS Desktop user, but an extra ArcObjects interface is provided for easy programmatic access to the log.
Design Subtype of WorkspaceExtension abstract class
License required ArcEditor
Libraries Geodatabase, System
Languages Visual Basic, Visual C++
Categories ESRI Geodatabase Workspace Extensions (optionally)
Interfaces IWorkspaceExtension, IWorkspaceExtensionControl, and IWorkspaceHelper
How to use

274

Choose a test geodatabase for the workspace extension. It should be a geodatabase that you do not mind adding
a table to and also one that other users will not require access to for the duration of your test.

1.

Register the ConnectLog DLL on your client PC:

2.

If using VB, register ConnectLogVB.dll.


If using VC++, open and build the project ConnectLogVC.dsp to register the DLL.
3.

In ArcCatalog, enter the VBA environment and load the ConnectLogVBA.bas file. This file contains two macros
that you will use. From the Tools menu, add a reference to the ConnectLog DLL.

4.

Choose your test geodatabase and run the 'RegisterConnectLog' VBA macro. If you are testing with an enterprise
geodatabase, you will need to be connected as the 'sde' user to successfully run this script.

5.

If using VC++ you will need to edit the macro to comment out code specific to the VB example and comment in
the code specific to the VC++ example.

6.

If you are testing with an enterprise geodatabase, locate the DevSamples_ConnectLog table (that will have just
been created). Right-click on it and choose Privileges. Assign SELECT and INSERT permissions to your test users.

7.

Shut down and reopen ArcCatalog. If testing with an enterprise geodatabase, you can just disconnect and
reconnect.
Run the TestConnectLog VBA macro. Open the VBA 'Immediate' window to see a list of the connections made.

8.

If using VC++ you will need to edit the macro to comment out code specific to the VB example and comment in
the code specific to the VC++ example.
To tidy up, run the 'UnregisterConnectLog' VBA macro. Shut down and restart ArcCatalog. You will now be able to
see the DevSamples_ConnectLog table, which you may inspect and delete.

9.

If using VC++ you will need to edit the macro to comment out code specific to the VB example and comment in
the code specific to the VC++ example.

The case for a connection log workspace extension


Imagine you would like to keep track of who accesses your geodatabase and when. You would like a connection log
table in which a new row is inserted every time a user connects to the database. A simple way of doing this is to
implement a workspace extension. This is much easier than the alternative of customizing ArcMap and probably
ArcCatalog as well.

Capturing the connection event


When a workspace object is created, it cocreates any registered workspace
extensions and calls IWorkspaceExtensionControl::Init. The workspace object is
created whenever the user connects to the database, so the Init method is a suitable
place for code that inserts a row into the connection log.
[Visual Basic 6]

Implements IWorkspaceExtension
Implements IWorkspaceExtensionControl
...
Private Sub IWorkspaceExtensionControl_Init(ByVal pWorkspaceHelper As
esriGeoDatabase.IWorkspaceHelper)
Set m_pWorkspaceHelper = pWorkspaceHelper
...
' Open the table, it might not yet exist.
Dim pFeatWorkspace As IFeatureWorkspace
Set pFeatWorkspace = pWorkspaceHelper.Workspace
Dim pConnectLogTable As ITable
On Error Resume Next
Set pConnectLogTable = pFeatWorkspace.OpenTable(m_sQualifiedName)
On Error GoTo 0
If pConnectLogTable Is Nothing Then
Set pConnectLogTable = TryToCreateTable(pFeatWorkspace)
...
End If
' Add an entry to the connection log. Note, for Enterprise geodatabases,
' it is better to use the database date for the timestamp, but for
' simplicity this sample will just use the client OS date
Dim pRow As IRow
Set pRow = pConnectLogTable.CreateRow
pRow.Value(lUserField) = GetUser(pFeatWorkspace)
pRow.Value(lTimestampField) = Now
pRow.Store
End Sub

275

Note that the example stores a class-level reference to the workspace helper rather than the workspace itself. This
mechanism is similar to the class helper used by class extensionsit helps avoid circular references.
Although workspace extensions can be used to capture the connection event, the disconnection event cannot be
reliably capturedit is undesirable to have code that must be executed when the user disconnects by quitting the
application.

Hiding data dictionary tables from users


In the example, imagine that only administrators are interested in the connection log table. You would like to prevent
the table from appearing in ArcCatalog for other users.
You can do this by implementing the IWorkspaceExtension property DataDictionaryTableNames. This property returns
a list of tables that should be hidden. For enterprise geodatabases, the example chooses not to hide the connection log
table from the administrator (the 'sde' user). For personal geodatabases, it is hidden from all users. For enterprise
geodatabases, the table names returned by DataDictionaryTableNames should be fully qualified (that is, with the
owner and, if necessary, the database).
The PrivateDatasetNames property is similar to DataDictionaryTableNames, but is normally used for hiding datasets
that belong to the data schema or the connected user, rather than the system. As an example, consider the database
tables that lie behind a geometric network dataset. The GDB_GeomNetworks table is at the system level and so would
be appropriate to hide with DataDictionaryTableNames; however, the N_* tables for a particular geometric network
belong to the GIS data and so would be hidden with PrivateDatasetNames. The names returned by
PrivateDatasetNames are usually unqualified. An additional aspect of PrivateDatasetNames is the capability to filter out
feature datasets, feature classes, and relationship classes, as well as tables.
PrivateDatasetNames can be used to prevent feature datasets, feature classes, and relationship
classes appearing in browsers.
When implementing DataDictionaryTableNames or PrivateDatasetNames, you are confronted with a problem: both of
these properties must return an object that implements IEnumBSTR. There are no ArcObjects classes that implement
IEnumBSTR, so you have to provide your own. The example implementation is quite straightforward. A point to note is
that IEnumBSTR::Next is supposed to raise S_FALSE at the end of the enumeration. Visual Basic cannot return a
genuine HRESULT of S_FALSE since it corresponds to a positive integer which is incompatible with Visual Basic's error
handling. However, the ArcGIS framework will correctly interpret the results of the code below.
[Visual Basic 6]

Implements IEnumBSTR
Private Const S_FALSE As Long = 1
Private m_index As Long
Private colStrings As Collection
' Helper sub to add a string to the EnumBSTR
Public Sub Add(ByVal sString As String)
colStrings.Add sString
End Sub
...
Private Function IEnumBSTR_Next() As String
If m_index > colStrings.Count Or m_index < 1 Then
IEnumBSTR_Next = ""
Err.Raise S_FALSE
Else
IEnumBSTR_Next = colStrings.Item(m_index)
m_index = m_index + 1
End If
End Function
The Visual C++ code for the example implements an EnumBSTR object by using a template class: CSimpleArray2. This
is the same as the ATL CSimpleArray template class but with a bug fix for the RemoveAt function. This template class
could also be used to implement enumerators for arbitrary COM objects, that is, enumerators returning interface
pointers rather than data types such as BSTR.
One remaining issue with the data dictionary table is that of privileges. All users need insertion rights to the table.
Ideally these would be granted at the same time as the table is created. For simplicity, although the example creates
the table, it does not grant privileges as it is difficult to write generic code for this task that will run on all the possible
DBMSs. With the example, the privileges can be granted manually from the ArcCatalog GUI by the 'sde' user, since for
this user the table is not hidden (for Oracle the insert privilege can be granted to the 'public' role).

Implementing your own interface


Imagine that in the example you would like to provide some sort of viewing utility for the connection log. Even though
the table is hidden from users in the GUI, there is nothing to stop the table from being accessed by code with
IFeatureworkspace::OpenTable. However this requires the client code to know the fully qualified name of the table. A
nice facility would be to provide access to the table from the workspace extension.

276

To this end, a new interface is defined in the example, IConnectLog, which provides one method, GetConnections,
which returns a cursor on the connection log. Below is an example of how IConnectLog might be used from a client.
Note how the extension is found by its ProgID using the IWorkspaceExtensionManager interface, which is implemented
by geodatabase workspaces.
[Visual Basic 6]

Dim pWorkspaceExtManager As IWorkspaceExtensionManager


Set pWorkspaceExtManager = pDataset.Workspace
If pWorkspaceExtManager Is Nothing Then
MsgBox "This workspace does not support workspace extensions"
Exit Sub
End If
Dim pUID As esriSystem.IUID
Set pUID = New esriSystem.UID
pUID.Value = "ConnectLogVB.WorkspaceExt"
Dim pConnectLog As ConnectLogVB.IConnectLog
Set pConnectLog = pWorkspaceExtManager.FindExtension(pUID)
If pConnectLog Is Nothing Then
MsgBox "Connect Log Workspace Extension not found"
Exit Sub
End If
Dim pCursor As ICursor
Set pCursor = pConnectLog.GetConnections()
Dim pRow As IRow
Set pRow = pCursor.NextRow
Do Until pRow Is Nothing
Debug.Print pRow.Value(1), pRow.Value(2)
Set pRow = pCursor.NextRow
Loop

Workspace Property Pages


Imagine that when an ArcCatalog user right-clicks the geodatabase and opens the Database Properties dialog box, you
would like an extra tab to appear displaying the latest connections to the database.
This could be done by implementing a property page and registering it to the ESRI Workspace Property Pages
component category. For simplicity, the Connection Log example does not implement a property page.
A workspace property page may often be appropriate for a workspace extension.
See Also About Workspace Extensions and Managing Workspace Extensions.

Managing Workspace Extensions


There are two alternative ways of deploying a workspace extension: registering with a geodatabase or registering to
the ESRI Geodatabase Workspace Extensions component category. If you register to the component category your
extension will be applied to all geodatabases accessible from that client. If instead, you register the workspace
extension to a geodatabase, the extension is deployed only against that geodatabase for all clients.
There are two alternative deployment methods for a workspace extension: registering with a
geodatabase, or registering to a component category.
It is normally preferable to use geodatabase registration for workspace extensions, since it typically doesn't make
much sense to have the extension apply to all possible enterprise and personal geodatabases that a user may connect
to. This is particularly true if the workspace extension causes data to be written, such as the Connection Log example.
The major caveat with geodatabase registration is that if the workspace extension fails unexpectedly, or if its DLL is
unavailable, the geodatabase cannot be opened.
To register a workspace extension with a geodatabase, use IWorkspaceExtensionManager::RegisterExtension. The
ArcCatalog VBA code below registers the example with the selected geodatabase.
[Visual Basic 6]

Public Sub RegisterExampleWorkspaceExtension()


Dim pGxApp As IGxApplication
Set pGxApp = Application
Dim pGxObject As IGxObject
Set pGxObject = pGxApp.SelectedObject
If TypeOf pGxObject Is IGxDatabase Then
Dim pGxDatabase As IGxDatabase

277

Set pGxDatabase = pGxObject


If Not (TypeOf pGxDatabase.Workspace Is IWorkspaceExtensionManager) Then
MsgBox "This workspace does not support registration" & vbNewLine _
& "of workspace extensions."
Exit Sub
End If
Dim pWorkspaceExtMgr As IWorkspaceExtensionManager
Set pWorkspaceExtMgr = pGxDatabase.Workspace
Dim pUID As New UID
pUID.Value = "ConnectLogVB.WorkspaceExt"
pWorkspaceExtMgr.RegisterExtension "ConnectLog VB example", pUID
MsgBox "Workspace extension registered"
End If
End Sub
Calling RegisterExtension requires geodatabase DBA privileges (for an enterprise geodatabase, this normally means
connecting as the 'sde' user). As a matter of interest, registering your workspace extension in this way inserts a row
into GDB_EXTENSIONS, one of the geodatabase system tables.
Registering a workspace extension with a geodatabase inserts a row into GDB_EXTENSIONS.
The Name parameter to RegisterExtension can be whatever you like. It is used in the error description if your
extension fails to load.
To unregister a workspace extension from a geodatabase, use IWorkspaceExtensionManager::UnRegisterExtension. Of
course, to get the IWorkspaceExtensionManager interface, the workspace must already be open, so you will need the
workspace extension DLL registered on the client machine that unregisters the extension from the geodatabase.
Managing multiple workspace extensions
When a user connects to a geodatabase, the workspace instantiates all workspace extensions found in the ESRI
Geodatabase Workspace Extensions component category. It then instantiates any additional extensions that are
registered with that geodatabase. Thus it is possible for multiple workspace extensions to be active at a time. You can
use IWorkspaceExtensionManager to discover the active extensions. You may notice that geodatabases have one
standard workspace extension (codenamed Titus) that implements some of the topology functionality. Do not
unregister this extension from the geodatabase.
See Also About Workspace Extensions and ConnectLog Workspace Extension Example.

About OLE DB Providers


Microsoft's OLE DB model enables external data to be served to ArcGIS applications by means of an OLE DB provider.
The following sections discuss implementing your own OLE DB provider to serve spatial data from a custom data
source to ArcGIS or any other application. The following sections do not cover the standard ESRI OLE DB provider
which provides ESRI format data to external applications; this, and the consumption of OLE DB data in ArcGIS, is
discussed in Implementing OLE DB Providers in ArcGIS.
OLE DB providers enable external data to be served to ArcGIS applications.
An OLE DB provider for spatial data should use the standards defined by the Open GIS Consortium (OGC or OGIS).
These are described in the 'OpenGIS Simple Features Specification for OLE/COM', which is available from
www.opengis.org.
The Open GIS Consortium defines standards for spatially enabled OLE DB providers.
In addition to the OGIS requirements, there are the Microsoft requirements for a 'Minimum level provider' (known as
level 0). These are set out in the Microsoft article 'OLE DB Leveling: Choosing the Right Interfaces'. Before continuing
with the details of implementing an OGIS-compliant OLE DB provider, consider the two main alternative solutions for
serving a custom data source to ArcGIS: custom layers and plug-in data sources.
Custom layers and plug-in data sources are alternatives to OLE DB providers for serving custom data
formats to ArcGIS. See the comparison table in the plug-in data sources section.
Both of these approaches have their own sections in this book. There is a table summarizing the benefits of each
solution in the section on plug-in data sources.
ArcGIS Requirements for OLE DB providers
To be consumable by ArcGIS, in addition to the Microsoft minimum provider functionality, your provider must satisfy
the following requirements (they will be explained in more detail later):
1.

Implement one standard schema rowset, the Tables rowset, using the IDBSchemaRowset interface.

2.

Implement the OGIS Feature Tables and Geometry Columns schema rowsetsthey are needed for browsing and
schema discovery.

3.

Support the ICommandWithParameters interface on the Command objectthis is needed to handle spatial queries.

4.

Support the IColumnsRowset interface on the Rowset objectthis is needed to support the additional OGIS

278

Metadata columns: GEOM_TYPE, SPATIAL_REF_SYSTEM_ID, and SPATIAL_REF_SYSTEM_WKT.


5.

Return geometry objects as OGIS Well-Known Binaries (WKBs).

ArcGIS does not require the provider to be registered to the OGISDataProvider component category. It is also not
essential to implement the OGIS Spatial Reference Systems Schema rowset.
References
Several good information resources are listed below. They can mostly be found in MSDN:

Using the Visual C++ 6.0 OLE DB Provider Templates, Lon Fisher, Visual C++ Development Team, 1998. Required reading
msdn.microsoft.com/library/en-us/dnvc60/html/msdn_vc6oledbprov.asp

Exposing Your Custom Data In a Standard Way Through ADO and OLE DB, Dino Esposito, June 1999. Good article
www.microsoft.com/msj/0699/oledb/oledb.htm

OLE DB Leveling: Choosing the Right Interfaces. Required reference.


www.microsoft.com/data/oledb/techinfo/oledbleveling2.htm
See also the similar MDAC article OLE DB Minimum Levels of Consumer and Provider Functionality
msdn.microsoft.com/library/en-us/dnoledb/html/oledbleveling2.asp

OLE DB/ADO: Making Universal Data Access a Reality, Microsoft Corp., 1998. General reference
msdn.microsoft.com/library/en-us/dnuda/html/msdn_dbado.asp

OpenGIS Simple Features Specification For OLE/COM. Required reference


www.opengis.org/techno/specs.htm

MSDN: Visual C++ Reference, OLE DB Templates Reference. Useful reference


msdn.microsoft.com/library/en-us/vclib/html/vcrefOLEDBTemplates.asp

MSDN: Visual C++ Concepts, OLE DB Templates. Useful reference


msdn.microsoft.com/library/en-us/vccore/html/vcconOverviewOLEDBProviderTemplates.asp

Microsoft Universal Data Access Web Site. Portal


www.microsoft.com/data

See Also OGIS OLE DB Provider Example

OGIS OLE DB Provider Example


Object Model Diagram

279

Example Code Click here.


Description The example implements a spatially enabled OLE DB provider for personal geodatabases.
Design Microsoft OLE DB provider architecture
License required ArcView
Libraries Geodatabase
Languages Visual C++
Categories OGISDataProvider (not necessary for use by ArcGIS).
Interfaces OLE DB standard interfaces.
How to use
1.

Open and build the project SampleProvider.dsp, to compile and register the DLL (you will have to modify
the path to the ESRI type libraries in the StdAfx.h file to correspond to your ArcGIS install directory).

2.

Make an OLE DB connection from ArcGIS using the SampleProvider. Double click on the Add OLE DB
connection object in either ArcCatalog (TOC panel) or ArcMap (Add Data dialog box). This will call the Data
Link Properties dialog boxon the Provider tab choose the 'SampleProv OLE DB Provider'.

3.

Click Next to bring up the Connection tab and enter the path to and name of a personal geodatabase or
Microsoft Access .mdb file in the Data Source field. Click Next to move to the Advanced tab and click OK. A
new OLE DB connection should appear in the TOC panel or Add Data dialog box.

4.

Test the provider by browsing the data.

About the OGIS OLE DB provider example


This example demonstrates how to create a read-only OLE DB provider, which meets all the requirements for ArcGIS;
that is, it can identify, query, and retrieve spatial data.
The sample provider can read data from an ArcGIS personal geodatabase or Microsoft Access .mdb file (don't confuse
this example with the standard ESRI OLE DB provider, which provides access to data in various ESRI formats). The
example follows the standards in the OpenGIS (OGIS) OLE-COM Simple Features Specification.
The example serves personal geodatabase data via OLE DB. There is already a standard ESRI OLE
DB provider to do this job, but the example exists to show how to implement OLE DB providers for
spatial data.

The following description assumes that you have a working knowledge of Microsoft's Component Object Model (COM)
technology and that you are familiar with Microsoft's OLE DB data access technology. To create an OLE DB provider,
you should have a working knowledge of C++ templates.

Starting to develop an OLE DB provider


There are a few options for developing your own provider.

Use Microsoft Visual C++ OLE DB templates to create your provider. At Visual C++ 6.0 these templates
supported just read-only providers. With Visual C++ .NET they also support updatable providers.

Use Microsoft's simple OLE DB provider toolkit in the Data Access 2.x SDK for creating read-only providers.

Use a third party OLE DB provider toolkit, for example, http://www.simba.com/index.htm.

Write a provider in C++ using standard ATL classes.

280

The ATL Object wizard creates a provider using the OLE DB template classes.
This example was created by using the Visual C++ 6.0 OLE DB templates. A complete discussion of these templates is
beyond the scope of this book. See the Microsoft article 'Using the Visual C++ 6.0 OLE DB Provider Templates' for an
explanation of how to create a Visual C++ project from these templates using the ATL Object Wizard.
The resulting project will contain seven standard OLE DB COM objects (in three .h files). These objects are
implemented using the ATL data access templates (found in atldb.h from your Visual Studio installation folder).

Data Sourcea connection to your physical file or database.

Sessionthe current operating environment of your data source.

Commandan object used to issue commands (SQL statements) and create rowsets.

Rowsetan object that contains rows of data organized in columns.

Three Schema Rowsetsrows containing information about the schema of your data source.

Implementing the Data Source object


The Data Source object represents a connection to your physical file or database. Look in the example's
SampleProvDS.h file (when running the templates in a fresh project it will be in the <YourProvider>DS.h file).
[Visual C++]

class ATL_NO_VTABLE CSampleProvSource:


public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CSampleProvSource, &CLSID_SampleProv>,
public IDBCreateSessionImpl<CSampleProvSource, CSampleProvSession>,
public IDBInitializeImpl<CSampleProvSource>,
public IDBPropertiesImpl<CSampleProvSource>,
public IPersistImpl<CSampleProvSource>,
public IInternalConnectionImpl<CSampleProvSource>
{
...
}
A Data Source object must support the important IDBCreateSession, IDBInitialize, and IDBProperties interfaces. Note
how the class implements these by inheriting from the template classes IDBCreateSessionImpl, IDBInitializeImpl, and
IDBPropertiesImpl.
One of the class's main responsibilities is to open (initialize) and close (uninitialize) your data source. To do this the
example code overrides template class implementation of the IDBInitialize::Initialize method.
The code creates an Access workspace factory and opens the database. The name of the database is obtained from the
DBPROPSET_DBINIT property in the DBPROP_INIT_DATASOURCE property set. The data source name is set by the
OLE DB consumer, in this case ArcGIS, using the IDBProperties interface. This happens when you make an OLE DB
connection to this provider using the Data Link dialog box.
The data source properties are derived from the Data Link dialog.
[Visual C++]

STDMETHOD(Initialize)(void)
{
HRESULT hr;
if (FAILED(hr = IDBInitializeImpl<CSampleProvSource>::Initialize()))
return hr;
// Get the database property from the OLE DB properties

281

DBPROPIDSET propIDSet;
DBPROPID

propID = DBPROP_INIT_DATASOURCE;

propIDSet.rgPropertyIDs
propIDSet.cPropertyIDs

= &propID;
= 1;

propIDSet.guidPropertySet = DBPROPSET_DBINIT;
ULONG nProps;
DBPROPSET* propSet = 0;
if (FAILED(hr = GetProperties(1, &propIDSet, &nProps, &propSet)))
return E_FAIL;
IPropertySetPtr ipConnProps(CLSID_PropertySet);
ipConnProps->SetProperty(CComBSTR(OLESTR("DATABASE")),
propSet->rgProperties[0].vValue);
::VariantClear(&propSet->rgProperties[0].vValue);
::CoTaskMemFree(propSet->rgProperties);
::CoTaskMemFree(propSet);
// Create an Access WorkspaceFactory and open the Workspace
IWorkspaceFactoryPtr ipAccessWSF(CLSID_AccessWorkspaceFactory);
if (FAILED(hr = ipAccessWSF->Open(ipConnProps, 0, &m_ipWS)))
return E_FAIL;
return hr;
}
In IDBInitialize::Uninitialize, the sample frees the Workspace object, which is connected to the Access database.
You may want your Session object to maintain references to objects that the Data Source holds onto (for efficiency
reasons). In the sample IDBCreateSession::CreateSession is implemented to pass the workspace object to the
Session.
Because the Data Source, Session, Command and Rowset objects all work closely together, often in parent/child
relationships, and because they are only exposed through COM, the example declares their member variables as public
so that each class can reference them with simplicity.
Note that the sample imports some of the standard ESRI type libraries in its stdafx.h file, so it can use ArcObjects for
its implementation. This is only necessary because ArcObjects is convenient when implementing a provider for
personal geodatabases. Normally you will not need to import these type libraries since ArcObjects will not be relevant
to your data source.
The example uses ArcObjects for convenience. ArcObjects is not necessary for implementing OLE DB
providers.

Implementing the standard schema rowsets


Schema rowsets contain information about the structure of your data source. The ATL Object Wizard creates
implementations for the three standard OLE DB schema rowsets: Tables, Columns, and Provider Types. They are
located in the SampleProvSess.h file.
The Tables schema rowset contains a list of all the tables in your data source. The templates provide the
C<YourProvider>SessionTRSchemaRowset class. The example implements this rowset by using
IWorkspace::DatasetNames to get a list of the datasets in the database, then populating the template objects with this
information. A CTABLESRow object is used for each table found (this class is defined in Atldb.h). Note, from the three
standard schema rowsets, ArcGIS uses only the Tables schema rowset.
The Columns schema rowset contains a list of all the columns and their respective tables in your data source. The
templates provide the C<YourProvider>SessionColSchemaRowset class. The example does not implement this schema
rowset. Although ArcGIS does not require this schema rowset, if you plan to access your data via other clients or from
ADO, you should implement this rowset.
The Provider Types schema rowset contains a list of all the data types that your data source supports. The templates
provide the C<YourProvider>SessionPTSchemaRowset class. The example does not implement this schema rowset.

Implementing the OGIS schema rowsets


ArcGIS uses the OGIS schema rowsets to discover what tables are spatially enabled and other spatial information.
The Feature Tables schema rowset is mandatory. It contains a list of the feature classes in your data source. You will
need to create a row class to hold information about each feature class. In the example, this class is called

282

OGISTables_Row. The Provider Column Map macros make the implementation fairly simple.
[Visual C++]

class OGISTables_Row
{
public:
WCHAR

m_szAlias[4];

WCHAR

m_szCatalog[4];

WCHAR

m_szSchema[4];

WCHAR

m_szTableName[129];

WCHAR

m_szColumnName[129];

WCHAR

m_szDGName[129];

OGISTables_Row()
{
m_szAlias[0] = L'\0';
m_szCatalog[0] = L'\0';
m_szSchema[0] = L'\0';
m_szTableName[0] = L'\0';
m_szColumnName[0] = L'\0';
m_szDGName[0] = L'\0';
}
};
The OGISTables_Row class is used as the array element to implement the Feature Tables schema
rowset.
The class for the schema rowset itself is CSampleProvSessionSchemaOGISTables. The Execute function populates the
rowset.
The Geometry Columns schema rowset is also mandatory. It contains a list of the geometry columns and their
associated feature classes in your data source. In the example, the OGISGeometry_Row class holds information about
a geometry column. The class for the schema rowset is CSampleProvSchemaOGISGeoColumns.
The Spatial Reference schema rowset contains a list of the spatial references in your data source. ArcGIS doesn't use
this schema rowset. If you want to implement this class, the example code contains a basic definition,
CSampleProvSessionSchemaSpatRef, and a row class, OGISSpat_Row, to hold information about a spatial reference.

Implementing the Session object


The Session object represents the current operating environment of your data source. The example does not need to
override any of the template's implementation; however, this object does contain the ATL Schema Map that defines
which schema rowsets exist. In the example, see the CSampleProvSession class:
The Session object defines which schema rowsets exist in your provider.
[Visual C++]

BEGIN_SCHEMA_MAP(CSampleProvSession)
SCHEMA_ENTRY(DBSCHEMA_TABLES, CSampleProvSessionTRSchemaRowset)
SCHEMA_ENTRY(DBSCHEMA_COLUMNS, CSampleProvSessionColSchemaRowset)
SCHEMA_ENTRY(DBSCHEMA_PROVIDER_TYPES, CSampleProvSessionPTSchemaRowset)
SCHEMA_ENTRY(DBSCHEMA_OGIS_FEATURE_TABLES,CSampleProvSessionSchemaOGISTables)
SCHEMA_ENTRY(DBSCHEMA_OGIS_GEOMETRY_COLUMNS,CSampleProvSchemaOGISGeoColumns)
SCHEMA_ENTRY(DBSCHEMA_OGIS_SPATIAL_REF_SYSTEMS,CSampleProvSessionSchemaSpatRef);
END_SCHEMA_MAP()
The GUIDs of the OGIS schema rowsets are contained in the OleDBGis.h file (available from www.opengis.org). The
guids.cpp file in the example forces these GUID definitions to be compiled into the program thus avoiding link errors.
Note that in the project settings for guids.cpp, it is set to 'Not using precompiled headers' (under the C/C++ tab).

Implementing the Command object


The Command object is used to issue commands (SQL statements) and create rowsets. It is implemented in the
SampleProvRS.h and .cpp files.
In the OLE DB specification, the purpose of the Command object is mostly for database optimization, so that a SQL
statement can be created once, optionally prepared, and reexecuted with optional parameters to create new rowsets.
The ATL Object Wizard provides a standard Command class. In the example, the main modification is in the
implementation of ICommmand::Execute. Note that this ICommand is a different interface than
esriSystemUI.ICommand. In fact, when esriSystemUI.olb is imported in the example, ICommand is renamed to
IESRICommand to avoid any possible clash.
The example implementation of ICommand::Execute sets a query filter, opens the table, and creates the Rowset

283

object that is the result of executing the command. References to the table and query filter objects are cached as class
members to provide efficiency. These references are freed when new command text is set by the implementation of
ICommandText::SetCommandText.
Queries can have spatial criteriathese are specified as parameters to the Command object. In this case you must
implement ICommandWithParameters. The example provides a C++ template class for implementing this interface:
ICommandWithParametersImpl. The CSampleProvCommand class inherits from this template. The template does not
provide a complete implementation of ICommandWithParameters; it only handles the OGIS spatial parameters.
However, this is enough to support the requirements of the example.
The ICommandWithParameters interface is used to access OGIS-compliant spatial query criteria.
The implementation of ICommand::Execute processes spatial parameters using a helper function: SetupSpatialFilter.
The example needs to convert the OGIS WKB back to an ESRI geometry object since the data source is an ESRI
personal geodatabase. You will probably not need to do this, since it is unlikely your data source will use ESRI
geometry objects. Note that the OGIS spatial filter operators (touches, within, and so on) are defined in the
OleDBGis.h file.

Implementing the Rowset object


The Rowset object contains rows of data organized in columns. It is defined in the SampleProvRS.h file.
[Visual C++]

class CSampleProvRowset :
public CRowsetImpl< CSampleProvRowset,
CSampleProvFeatureRowData,
CSampleProvCommand,
CVirtualArray<CSampleProvFeatureRowData>,
CSimpleRow>,
public IColumnsRowsetImpl<CSampleProvRowset, CSampleProvCommand>
{
...
};
The ATL template class CRowsetImpl provides most of the implementation. A parameter to the template is the storage
class that will represent one row of data. For this purpose, the example implements the CSampleProvFeatureRowData
class. Compare the implementation of this class with the OGISTables_Row and CTABLESRow classes previously
discussed; once again the Provider Column Map does most of the work.
[Visual C++]

BEGIN_PROVIDER_COLUMN_MAP(CSampleProvFeatureRowData)
PROVIDER_COLUMN_ENTRY("OID", 1, m_oidColumn)
PROVIDER_COLUMN_ENTRY("SHAPE", 2, m_shapeColumn)
END_PROVIDER_COLUMN_MAP()
For simplicity, the example deals with only the OID and SHAPE columns because they are present in every
geodatabase feature class. The example does not handle tables not registered with the geodatabase, that is, those
that have no OID column.
If your data has a fixed schema, as is often the case, this mechanism will work well for you. If your schema varies
from table to table, then you will need a more elaborate row class than the one presented here. For some ideas on
doing this, see the Microsoft ATLMTO sample in MSDN.
Another parameter to CRowsetImpl is the array type that will represent the set of rows. By default the ATL
CSimpleArray template class is used. For datasets of any significant size, CSimpleArray will not be suitable since the
entire dataset will be loaded into memory. As an improvement to this, the example defines a CVirtualArray template
class, which wraps an ArcObjects cursor so that only the current record is held in memory at one time. It is this class
that retrieves the ESRI Geometry for the row and converts it to an OGIS WKB. Your provider will create the WKB from
whatever format your data source's geometry is stored in.
When implementing Rowsets, the array class implementation is important for performance.
The example's main code for Rowset is in the Execute method. The CVirtualArray object is initialized with a cursor
representing the results of the Command on the table and the number of rows that the cursor will return. Note that
ATL must have this row countyou should take care that it is calculated as efficiently as possible.
CSampleProvRowset also inherits from the template class IColumnsRowsetImpl. The standard OLE DB mechanism for
obtaining column definitions is to use IColumnsInfo::GetColumnInfo. However, this method returns fixed definitions
for the column descriptions; the IColumnsRowset interface exists to allow for more flexible column metadata reporting.
Additionally, OLE DB consumers can get the column information directly from a rowset without having to return to the
Session object and IDBSchemaRowset. Implementation of IColumnsRowset is mandatory for OGIS-compliant
providers.
The OGIS specification defines additional metadata columns: GEOM_TYPE, SPATIAL_REF_SYSTEM_ID and
SPATIAL_REF_SYSTEM_WKT, so that consumers can identify the spatial column containing the WKB geometry, what
its geometry type is, and what spatial reference system it belongs to.
The example defines the IColumnsRowsetImpl template class as a way of implementing IColumnsRowset. It uses
CColumnsRowsetRow as a helper class. Examine the GetColumnsRowset and PopulateRowset functions; you will need

284

to replace this code in your provider.


The example does not pass back the OGIS Spatial Reference System ID or the WKT (see the example's GetDBStatus
function) because these are currently expensive to obtain in ArcObjects. Your provider should attempt to support
these.
See Also About OLE DB Providers.

Chapter 8: Extending the Editor


Extending the editing framework
The standard Editor toolbars and environment does a great job in solving most people's editing needs. Sometimes,
however, it is necessary to create a custom tool that provides some missing functionality or links several operations
together. This chapter demonstrates a number of custom object solutions for basic editing scenarios to illustrate how
to extend the editing model.
Editing customizations typically fall into one of the following categories: macros, commands, tools, edit tasks, snap
agents, feature inspectors, and extensions. This chapter focuses on how to use each category to solve specific editing
problems. The customizations discussed, therefore, rely on the use of the editing framework within the ArcMap
application and require the use of an ArcEditor license.
Using Macros
Using Macros
Using macros to perform editing customizations
Commands and Tools
Editor Commands and Tools
Introduction to creating and using commands and tools for the Editor menus, commandbars, and toolbars
Difference Command Example
An example of an Editor command that applies a difference operation to two features
Split At Intersection Tool Example
An example of an Editor tool that splits a polyline into two based on an intersection with a tracked line shape
Edit Tasks
About Edit Tasks
Introduction to creating custom edit tasks
Construct Point Edit Task
An example of an edit task which creates a new point feature at the end of an edit sketch
Snap Agents and Editor Extensions
About Snap Agents
Introduction to creating custom snap agents
About Editor Extensions
Introduction to creating extensions to the editing framework
Subtypes Snap Agent Example
An example of a snap agent that allows you to snap to subtypes of a feature when editing. This example also provides
a dockable window to help manage the snapping properties, a command to show and hide the window, and an editor
extension to tie all the custom classes together.
Feature Inspectors
Custom Feature Inspectors
Introduction to creating your own custom feature inspectors
Tabbed Feature Inspector Example
An example of a feature inspector that provides a tabbed form upon which users can view both the standard feature
inspector and also a geodatabase raster image attribute.

Using Macros
Macros Using the Editor
Although macros don't truly extend the editor model (or ArcObjects for that matter), they are worth discussing
because they are commonly used to add new editing functionality to a map document quickly.
Macros are perfect for problems that don't require much coding. However, they can still be quite complex, UIControls
are macro-based and can nearly mimic a custom command or tool, but they have some drawbacks. They are hard to
share with others or even between different map documents, and the code is viewable. You may want to convert your
macros to commands or tools if you need to share them or if you use them with multiple map documents. Ease of
debugging is a macro's biggest benefit; many commands and tools start off as macros simply because macros are so

285

much easier to debug.

A Simple Macro Scenario


When digitizing on top of an aerial photograph, it is hard to see the edit sketch; it would be better if the edit sketch
vertices and segments were larger and perhaps had a different color. Since the editing environment does not persist
the sketch symbology, writing a macro is a perfect solution.
Solution
This is not a complicated scenarioand one reason why a macro solution may be appropriate. Below is a macro that
modifies the edit sketch properties.
[Visual Basic 6]

Public Sub ChangeSketchSymbol()


Dim pEditor As IEditor
Dim pEditProperties As IEditProperties
Dim pLineSymbol As ILineSymbol
Dim pMarkerSymbol As ISimpleMarkerSymbol
Dim pRgbColor As IRgbColor
Dim pID As New UID
' Get a reference to the Editor object
pID = "esriEditor.Editor"
Set pEditor = Application.FindExtensionByCLSID(pID)
Set pEditProperties = pEditor

' Query Interface

'Change the vertex size and color


Set pMarkerSymbol = New SimpleMarkerSymbol
Set pRgbColor = New RgbColor
pRgbColor.Blue = 255
pMarkerSymbol.Color = pRgbColor
pMarkerSymbol.Size = 6
Set pEditProperties.SketchVertexSymbol = pMarkerSymbol
' Change the sketch symbol to use a red line
Set pLineSymbol = New SimpleLineSymbol
Set pRgbColor = New RgbColor
pRgbColor.Red = 255
pLineSymbol.Color = pRgbColor
Set pEditProperties.SketchSymbol = pLineSymbol
End Sub
Instead of having the color hardcoded, try enhancing the macro to use a color dialog box picker.

Other editing scenarios that can be solved using a macro


Another useful macro is one that applies global changes to attributes; for example, a macro that changes the case of
records stored in a text property. However, macros aren't only used for simple, one-function routines; almost any
command or tool can be written as a macro in VBA.
Edit Tasks
Edit tasks are similar to edit commands with one major difference: edit tasks perform a specific operation using a
geometry, typically one which was created by a sketch tool. For example, the Create New Feature edit task creates
new features based on the geometry created by the various sketch tools; similarly, the Select Features Using a Line
edit task selects features in the map that are intersected by the edit sketch. In both cases, a geometry created by the
sketch tools is used to complete an operation.
Snap Agents
Snap agents facilitate geometry placement. For example, the sketch tools make use of snap agents to enable a user to
precisely place an edit sketch vertex. Because snap agents are so widely used, the editing framework manages a
snapping environmenta collection of snap agents and a snap tolerance (ISnapEnvironment::SnapTolerance).
Editor Extensions
It would be easy to say that editor extensions extend the editing framework, and although this is true, so do custom
commands, tools, edit tasks, and so on. Editor extensions are just another way developers can plug in to the editing
model and extend it. The difference between editor extensions and other customizations is that extensions are
automatically loaded and unloaded by the application; there is only ever one instance of an extension running at a
time.
See Also Extending the Editing Framework.

286

Editor Commands And Tools


Editor Commands
Commands appear in numerous places in the editor framework, providing a large part of the editors interactive
functionality; examples of editor commands include the Buffer, Intersect, and Union commands. As with any
commands, an edit command does not require the user to interact with the Map after being clicked.
Commands do not require you to interact with the map; instead, commands often rely on the current
state of the map.
The main editor commands reside on the Editor Menu, for example, the Start Editing and Stop Editing commands. The
majority of the remaining commands reside on one of the editors context menus; for example, the Delete Vertex and
the Finish Sketch commands.

All the items on the Editing menu, the Sketch Tool context menu, and the Edit Sketch context menu are implemented
as commands. Most of these commands perform an edit operation, but a few, such as Snapping, open a dialog box.
Like any other ArcGIS command, editing commands must implement the ICommand interface, although many
commands also sink the outbound IEditEvents interface (or the IEditEvents2, IEditEvents3 interfaces).
Editor commands are typically registered to the ESRI Mx Commands component category. To facilitate developers, the
editing framework also supports several additional component categories for editing commands:

ESRI EditTool Menu Commands for commands on the Edit Tools context menu.

ESRI Sketch Menu Commands for commands on the Edit Sketch context menu.

ESRI SketchTool Menu Commands for commands on the Sketch Tool context menu.

A command registered in one of these categories will automatically appear in its associated context menu. This
prevents users from having to set up their customized editing environment manually.
Seen below are the context menus for the Sketch tool, the edit sketch, and the Edit tool.

287

The editing framework additionally has numerous context menus for fixing topology errors. Each type of topology
violation has a context menu and a corresponding component category that maintains the list of commands on the
menu. You can automatically add new commands to a particular context menu by registering the command in the
appropriate component category. For example, a custom command that resolves a 'Point Inside Area' topology error
should be registered in the ESRI Point Inside Area Error Commands component category. There are too many
component categories for topology errors to list here but they all have names that start with one of the following: ESRI
Area, ESRI Line, or ESRI Point; below you can see a number of the component categories for Area error commands.

You can add commands to many of the editing and topology menus and toolbars by registering your
commands to the appropriate component categories.
You can see an example of an editor command, which performs a spatial operation upon two selected features in the
Difference Command Example. Other examples of editor commands you may want to create include:

Flip a polyline.
Delete selected features.
Delete edit sketch vertex.
Create features from selected graphics.
Attribute updates using advanced queries.

Editor Tools
Tools are nearly the same as commands, except they require you to interact with the map canvas after they have

288

been clicked. For example, the Split tool waits for you to select a point on the selected polyline feature, then breaks it
into two features.
Other edit tools include the sketch tools, the Edit tool, and the Rotate tool.
To better illustrate the difference between a command and a tool, look at the Move command versus a tool that moves
features. Once clicked, the Move command prompts you with a dialog box for a delta x and a delta y; after these
values have been entered, the selected features are moved. Aside from entering values in a dialog box, you don't
interact with the ArcMap canvas at all. In contrast, to reposition features with a move tool, such as the Edit tool, you
must interactively drag selected features across the display.

Edit tools typically reside on the Editor toolbar. General editing tools are registered in the ESRI MxCommands
component category, and they must implement both the ICommand and ITool interfaces.

The sketch tools are a specific type of editing tool. Sketch tools are always used to create an edit sketch which, when
completed, is passed onto the current task to perform a specific operation such as: creating a new feature, modifying
an existing feature, or simply selecting features. You can create a custom sketch tool and have it appear on the sketch
tool palette by registering the tool in the ESRI SketchTool Palette Commands component category.

You can see an example of an editor tool that splits an existing feature at a mouse-click location in the Split at
Intersection Tool Example.
See Also Extending the Editing Framework, Difference Command Example, and Split At Intersection Tool Example.

Difference Command Example

Description This project provides a custom editor command that will perform a difference operation on the two
features currently selected in a layer being edited. The result is applied to the geometry of the first feature, and the
second feature is deleted. The command will appear on the Editor menubar.
Design Coclass DifferenceCommand is a subtype of the Command abstract class and also sinks the IEditEvents
interface.
License required ArcEditor or above
Libraries Carto, Editor, Framework, Geodatabase, Geometry, System, and SystemUI

289

Languages Visual Basic


Categories ESRI Mx Commands
Interfaces ICommand, IEditEvents
How to use
1.

Register the DifferenceCommandVB.dll and double-click the DifferenceCommandVB.reg file to register to


component categories.

2.

Open ArcMap, click Tools, then click Customize.

3.

In the Customize dialog box, choose the Commands tab and click on 'Extending ArcObjects' in the left-hand
Categories list.

4.

In the Commands list, choose the Difference command, and drag this onto the Editor menu below the existing
Clip command. Click Close to dismiss the Customize dialog box.

5.

Add data with polygon features to ArcMap.

6.

Click on Editor and click Start Editing. Make sure that the polygon layer you added is editable and is the target
layer.

7.

Select two overlapping polygon features, or alternatively, use the Editor tools and commands to create some
overlapping features.

8.

Click on Editor and click Difference.


The two features you selected will be combined into one feature with the overlapping area removed.

The case for a difference command


The standard Editor menu provides a number of spatial operations that can be applied to multiple features, for
example, Union, Intersect, and Clip. However, if you want to merge two features into one feature, but remove the
overlapping area (the area that intersects), there is no command available to allow you to do this operation in one
step.

As no editor command provides the operation you require, you will create a custom editor command to meet your
requirements by performing a difference operation.
This example demonstrates how to create an editor command to perform a difference operation on
two features.

Creating an editor command

The previous topic, Editor Commands and Tools discussed how most Editor commands are implemented as a subtype
of Command by implementing the ICommand interface. Most editor commands also sink the IEditEvents outbound
interface from the Editor coclass, to respond to editing events.

Creating the DifferenceCommand

You can easily solve your requirements with a custom command that is similar to the Union, Intersect, and Clip
commands. A custom command is appropriate here because you can rely on another tool like the Edit tool to make the
selection beforehand, and your command will deal strictly with the task of performing the difference operation and
setting the geometry of the feature to be the result of the operation.
You will create a subtype of Command called DifferenceCommand by implementing ICommand. and sinking the
IEditEvents outbound interface from the Editor coclass.
Implementing ICommand
Begin by implementing OnCreate, where you will store references to the Application and Editor objects in the ArcMap
application.

290

[Visual Basic 6]

Private m_pApp As esriFramework.IApplication


Private m_pEditor As esriEditor.IEditor
Private m_pEditLayers As esriEditor.IEditLayers
Private Sub ICommand_OnCreate(ByVal hook As Object)
Dim pID As New esriSystem.UID
pID = "esriEditor.Editor"
Set m_pApp = hook
Set m_pEditor = m_pApp.FindExtensionByCLSID(pID)
If Not m_pEditor Is Nothing Then
Set m_pEditLayers = m_pEditor
End If
End Sub
To complete the enabled property, add a member variable m_bEnabled and return its value; you will set this value
later when implementing IEditEvents.
[Visual Basic 6] evemar

Private m_bEnabled As Boolean


Private Property Get ICommand_Enabled() As Boolean
ICommand_Enabled = m_bEnabled ' Check private member
End Property
Now you can perform the difference operation in the OnClick member using the variables you stored in OnCreate.
[Visual Basic 6]

Private Sub ICommand_OnClick()


On Error GoTo ErrorHandler:
Dim pTopoOp As ITopologicalOperator
Dim pGeoResult As IGeometry
Dim pActiveView As IActiveView
' Start an edit operation
m_pEditor.StartOperation
' Do the difference
Set pTopoOp = m_pFeature1.Shape
Set pGeoResult = pTopoOp.SymmetricDifference(m_pFeature2.Shape)
If pGeoResult Is Nothing Then GoTo ErrorHandler
' Delete the second feature and reset feature_1's shape
m_pFeature2.Delete
Set m_pFeature1.Shape = pGeoResult
m_pFeature1.Store
' Complete the operation and redraw new feature and selection
m_pEditor.StopOperation "Difference"
m_pEditor.Display.Invalidate m_pFeature1.Extent, True, 0
Set pActiveView = m_pEditor.Map
pActiveView.PartialRefresh esriViewGeoSelection, Nothing, Nothing
Exit Sub ' Exit sub to avoid error handler
ErrorHandler:
'In the event of an error, abort the operation
m_pEditor.AbortOperation
MsgBox "Difference command failed."
End Sub
Editor commands are usually displayed as a caption only, so you do not need to return anything from the Bitmap
member.
Implementing IEditEvents
By listening to the events on the IEditEvents interface, you can control access to your command. Commands are often
not enabled until a specific set of criteria is met; in this case, you can write the command so that it is only enabled
when the end user has selected two polygon features and set the target layer to a layer containing polygon features.

291

The best method for enabling a command based on selection criteria is to establish the command as an edit events
client responding specifically to IEditEvents::OnSelectionChanged and IEditEvents::OnCurrentLayerChanged.
1.

Begin by sinking the default interface of the Editor coclass, IEditEvents, in your class.
[Visual Basic 6]

Private WithEvents EditorEvents As esriEditor.Editor


2.

Add a line to the ICommand::OnCreate method to start listening to the events.


[Visual Basic 6]

Private Sub ICommand_OnCreate(ByVal hook As Object)


...
If Not m_pEditor Is Nothing Then
Set m_pEditLayers = m_pEditor
' Respond to IEditEvents interface.
Set EditorEvents = m_pEditor
End If
End Sub
3.

Create a function called SetEnabledStatus. In this function you need to verify that the user has selected only two
valid polygon features and has set the current target layer to a polygon feature class.
[Visual Basic 6]

Private Sub SetEnabledStatus()


Dim pEnumFeat As esriGeodatabase.IEnumFeature
' Assume command should not be enabled
m_bEnabled = False
' Make sure the current layer exists. When ArcMap is shutting down,
' the Editor fires various events, some of which we are listening to,
' but the Editor's properties have likely been emptied at this point.
If m_pEditLayers.CurrentLayer Is Nothing Then Exit Sub
' Check the target layer geometry type
If m_pEditLayers.CurrentLayer.FeatureClass.ShapeType = esriGeometryPolygon Then
' Analyze the Editor's selection
If m_pEditor.SelectionCount = 2 Then
Set pEnumFeat = m_pEditor.EditSelection
pEnumFeat.Reset
Set m_pFeature1 = pEnumFeat.Next
If m_pFeature1.Shape.IsEmpty Then Exit Sub
Set m_pFeature2 = pEnumFeat.Next
If m_pFeature2.Shape.IsEmpty Then Exit Sub
' If both features are polygons, enable the command
If m_pFeature1.Shape.GeometryType = esriGeometryPolygon And _
m_pFeature2.Shape.GeometryType = esriGeometryPolygon Then
m_bEnabled = True
Exit Sub
End If
End If
End If
End Sub
4.

Then implement the OnSelectionChanged and OnCurrentLayerChanged members of IEditEvents, and call the
SetEnabledStatus function from these members to set the Enabled status.
[Visual Basic 6]

Private Sub EditorEvents_OnCurrentLayerChanged()


SetEnabledStatus
End Sub
Private Sub EditorEvents_OnSelectionChanged()
SetEnabledStatus
End Sub
Now you can compile the DifferenceCommand project and use it in an ArcMap edit session.

292

See Also Editor Commands And Tools, and Split At Intersection Tool Example.

Split at Intersection Tool Example

Description This project provides a custom editor tool that can be used to perform a split operation on a polyline
feature, splitting the polyline at a point specified by another polyline tracked on the map using the tool. The tool will
appear on the Editor toolbar.
Design Coclass SplitToolAtIntersection is a subtype of the Tool abstract class and also sinks the IEditEvents interface.
License required ArcEditor or above
Libraries Carto, Display, Editor, Framework, Geodatabase, Geometry, System, and SystemUI
Languages Visual Basic
Categories ESRI Mx Commands
Interfaces ICommand, ITool, and IEditEvents
How to use
1. Register the SplitToolVB.dll and double-click the SplitToolVB.reg file to register to component categories.
2. Open ArcMap, click Tools, then click Customize.
3. In the Customize dialog box, click the Commands tab, then click on 'Extending ArcObjects' in the left-hand
Categories list.
4. In the Commands list, click the Split At Intersection tool and drag this onto the Editor toolbar. Click Close to
dismiss the Customize dialog box.
5. Add data with polyline features to ArcMap.
6. Click Editor and click Start Editing. Make sure that the polyline layer you added is editable and is the target layer.
7. Select a polyline feature, then click the Split At Intersection tool.
8. Track a line onto the map that intersects the feature at the location you want to perform the split. Double-click to
end the tracked line.
The polyline feature will now become two features.

The case for a split at intersection command


The standard Editor menu provides a split command which can be used to edit a polyline feature, creating two polyline
features by splitting the feature based on a click of the mouse at the desired split location.

293

If you are editing a polygon feature, you have a number of options for splitting a feature, including splitting the
polygon based on lines and polygons. However, if you have a polyline feature that you want to split, the existing tools
do not allow you to do this using a line tracked on scree. As no editor command or tool provides the operation you
require, you will create a custom editor tool to meet your requirements.
This example demonstrates how to create an editor tool to split a polyline feature into two features
at the location of an intersection with a tracked line.

Creating an editor tool

By reviewing the Editor object model diagram, you will see the EditTool class implements ICommand, ITool, and
IEditTool. IEditTool is an interface that provides a link to the source of edit events; however, this interface is not
implementable in VB.

As discussed previously in Editor Commands and Tools, a custom edit tool is created like any other tool, as a subtype
of the Tool abstract class, by implementing the ICommand and ITool interfaces. Like edit commands, edit tools often
sink the IEditEvents interface of the Editor, to respond to changes in the state of the Editor.

Creating the SplitAtIntersectionTool

You can solve this scenario with a custom tool. Tools are advanced commands; they additionally implement the ITool
interface, which allows them to respond to mouse and keyboard events. You will create a subtype of Tool called
SplitAtIntersectionTool by implementing ICommand and ITool and sinking the IEditEvents outbound interface from the
Editor coclass.
Implementing ICommand
The first member of ICommand to be called will be OnCreate. Store references to the Application and Editor objects in
this member.
[Visual Basic 6]

Private m_pApp As esriFramework.IApplication


Private m_pEditor As esriEditor.IEditor
Private m_pEditLayers As esriEditor.IEditLayers
Private Sub ICommand_OnCreate(ByVal hook As Object)
Dim pUID As New esriSystem.UID
Set m_pApp = hook

294

pUID = "esriEditor.Editor"
Set m_pEditor = m_pApp.FindExtensionByCLSID(pUID)
Set m_pEditLayers = m_pEditor
End Sub
Add a member variable to your command to keep track of the Enabled status of your command, then return the value
from the Enabled property. You will set this value later when implementing IEditEvents.
[Visual Basic 6]

Private m_bEnabled As Boolean


Private Property Get ICommand_Enabled() As Boolean
ICommand_Enabled = m_bEnabled ' Check private member
End Property
The remaining members of ICommand should be implemented as any other standard command, Name, Category,
Caption, and so on. Remember to specify a bitmap, as a tool is displayed by default using only its Bitmap. You do not
need to take any action in OnClick, as you will instead be using the ITool::OnMouseDown member to perform your
edit.
Implementing IEditEvents
By listening to the editor events you can control access to your tool. You should enable your tool when the end user
has selected one polyline feature, by handling the OnSelectionChanged event. (Unlike the DifferenceCommand, the
Editor's target layer is not important to the tool since its modifying an existing feature not creating a new one in the
target layer.)
1.

Begin by sinking the default interface of the Editor coclass, IEditEvents, in your class.
[Visual Basic 6]

Private WithEvents EditorEvents As esriEditor.Editor


2.

Add a line to the ICommand::OnCreate method to start listening to the events.


[Visual Basic 6]

Private Sub ICommand_OnCreate(ByVal hook As Object)


...
If Not m_pEditor Is Nothing Then
Set m_pEditLayers = m_pEditor
' Respond to IEditEvents interface.
Set EditorEvents = m_pEditor
End If
End Sub
3.

Create a function called SetEnabledStatus. In this function you need to verify that the user has selected a single
polyline feature.
[Visual Basic 6]

Private Sub SetEnabledStatus()


' Assume tool should not be enabled
m_bEnabled = False
' Make sure the current layer exists. When ArcMap is shutting down,
' the Editor fires various events, some of which we are listening to,
' but the Editor's properties have likely been emptied at this point.
If m_pEditLayers.CurrentLayer Is Nothing Then Exit Sub
'Only enable the tool when one polyline feature is selected
If m_pEditor.SelectionCount = 1 Then
Dim pEnumFeat As esriGeodatabase.IEnumFeature
Set pEnumFeat = m_pEditor.EditSelection
pEnumFeat.Reset
Dim pFeature As esriGeodatabase.IFeature
Set pFeature = pEnumFeat.Next
If pFeature.Shape.IsEmpty Then Exit Sub
If pFeature.Shape.GeometryType = esriGeometryPolyline Then
m_bEnabled = True
Exit Sub
End If

295

End If
End Sub
4.

Then implement the OnSelectionChanged member of IEditEvents, and call the SetEnabledStatus function from
this event handler to set the Enabled status.
[Visual Basic 6]

Private Sub EditorEvents_OnSelectionChanged()


SetEnabledStatus
End Sub
Implementing ITool
You can perform the main operation of your Tool in OnMouseDown.
OnMouseDown Part 1: calculating the intersection
In the first part of this function, you need to establish the two lines and their intersection points.
1.

First, you need to track a new line which will be used to split the existing feature. If the track operation is
unsuccessful, the intersection cannot take place, so exit the function.
[Visual Basic 6]

Private Sub ITool_OnMouseDown(ByVal Button As Long, ByVal Shift As Long, ByVal X As Long, ByVal
Y As Long)
Dim bInOperation As Boolean
Dim pLineSymbol As esriDisplay.ISimpleLineSymbol
Set pLineSymbol = New esriDisplay.SimpleLineSymbol
Dim pRubberLine As esriDisplay.IRubberBand
Set pRubberLine = New esriDisplay.RubberLine
Dim pPolyline As esriGeometry.IPolyline
Set pPolyline = pRubberLine.TrackNew(m_pEditor.Display, pLineSymbol)
If pPolyline Is Nothing Then Exit Sub
If pPolyline.GeometryType = esriGeometryType.esriGeometryNull Then Exit Sub
Set pPolyline.SpatialReference = m_pEditor.Map.SpatialReference
2.

Find the selected Polyline feature.


[Visual Basic 6]

Dim pEnumFeature As esriGeoDatabase.IEnumFeature


Set pEnumFeature = m_pEditor.EditSelection
pEnumFeature.Reset
Dim pFeature As esriGeoDatabase.IFeature
Set pFeature = pEnumFeature.Next
Dim pFeatureEdit As esriGeoDatabase.IFeatureEdit
Set pFeatureEdit = pFeature
3.

Next, you need to find the point where the feature and tracked line intersect. Create a function to perform the
intersection; see below for details of how the intersection was performed.

4.

Now call your intersection function from the OnMouseDown member; the geometry returned will always be a
point, unless the operation failed, in which case the result will be null.
[Visual Basic 6]

Dim pPointResult As IPoint


Set pPointResult = GetIntersection(pFeature.Shape, pPolyline)
If pPointResult Is Nothing Then Exit Sub
5.

Clear the current selection, and prepare to refresh the map by specifying a new Invalid area.
[Visual Basic 6]

Dim pActiveView As esriCarto.IActiveView


Set pActiveView = m_pEditor.Map
pActiveView.PartialRefresh esriViewGeoSelection, Nothing, Nothing
m_pEditor.Map.ClearSelection
Dim pInvalidArea As esriGeoDatabase.IInvalidArea
Set pInvalidArea = New esriCarto.InvalidArea
Set pInvalidArea.Display = m_pEditor.Display
pInvalidArea.Add pFeatureEdit
OnMouseDown Part 2: the edit operation

296

Now you can begin to perform the actual edit operation.


1.

Start a new edit operation.


[Visual Basic 6]

m_pEditor.StartOperation
bInOperation = True
2.

Perform the split operation using the IFeatureEdit::Split method.


[Visual Basic 6]

Dim pSplitSet As esriSystem.ISet


Set pSplitSet = pFeatureEdit.Split(pPointResult)
pSplitSet.Reset
3.

Iterate the results of the split operation, and display the results to the user by flashing the shape of each result
shape to the display. This is done by adding a new function to your class called FlashGeometry.
[Visual Basic 6]

Dim pFeatureSplit As esriGeoDatabase.IFeature


Set pFeatureSplit = pSplitSet.Next
Do While Not pFeatureSplit Is Nothing
FlashGeometry pFeatureSplit.Shape
Set pFeatureSplit = pSplitSet.Next
Loop
For the full details of the FlashGeometry function, see the sample project code.
4.

Now the split has been performed; stop the edit operation.
[Visual Basic 6]

' Stop the operation


m_pEditor.StopOperation "Split polyline"
bInOperation = False
pInvalidArea.Invalidate esriAllScreenCaches
5.

You should add an error handler to OnMouseDown to stop any edit operation if there is a problem during the
function.
[Visual Basic 6]

Private Sub ITool_OnMouseDown(ByVal Button As Long, ByVal Shift As Long, ByVal X As Long, ByVal
Y As Long)
On Error GoTo ErrorHandler:
...
Exit Sub ' Exit sub to avoid error handler
ErrorHandler:
If bInOperation Then m_pEditor.AbortOperation
MsgBox "Split failed"
End Sub
GetIntersection function
Finally, add the GetIntersection function that you called from the OnMouseDown member.
1.

Start by checking that the coordinate systems of the two lines are equal, and if not, project the polyline.
[Visual Basic 6]

Private Function GetIntersection(ByVal pIntersect As esriGeometry.IGeometry, ByVal pOther As


esriGeometry.IPolyline) As esriGeometry.IGeometry
Dim pClone As IClone
Set pClone = pIntersect.SpatialReference
If Not pClone.IsEqual(pOther.SpatialReference) Then
pOther.Project pIntersect.SpatialReference
End If
2.

Next, ensure that the polyline that was tracked on screen is Simple (another requirement for a spatial
operation).
[Visual Basic 6]

Dim pTopoOp As esriGeometry.ITopologicalOperator


Set pTopoOp = pOther
pTopoOp.Simplify
3.

Now perform the intersect operation.


[Visual Basic 6]

Set pTopoOp = pIntersec

297

Dim pGeomResult As IGeometry


Set pGeomResult = pTopoOp.Intersect(pOther, esriGeometry0Dimension)
If pGeomResult Is Nothing Then Exit Function
The intersect operation is performed by asking for point results, or zero-dimensional results.
4.

Check if there are multiple points of intersection. In this case, you can simply use the first point of intersection
to perform the split.
[Visual Basic 6]

If TypeOf pGeomResult Is IPointCollection Then


Dim pPointColl As esriGeometry.IPointCollection
Set pPointColl = pGeomResult
If pPointColl.PointCount >= 1 Then
Set pGeomResult = pPointColl.Point(0)
Else
Exit Function
End If
End If
If you wanted to adapt this example, you may want to perform a split into more than just two lines here, if more
than one intersection is found.

Now you can compile the SplitAtIntersectionTool project and use it in an ArcMap edit session.

See Also Editor Commands And Tools and Difference Command Example.

About Edit Tasks


Edit tasks are very similar to edit commands with one major difference: edit tasks perform a specific operation using a
geometrya geometry typically created by the sketch tools. For example, the Create New Feature edit task creates
new features based on the geometry created by the various sketch tools. Similarly, the Select Features Using a Line
edit task selects features in the map that are intersected by the edit sketch. In both cases, a geometry created by the
sketch tools is used to complete an operation.
Unlike a command or tool, edit tasks are not called directly by a user; instead, the Editor objects calls the currently
active edit task (IEditor::CurrentTask). The Editor calls the task's IEditTask::OnFinishSketch memberwhenever an
edit sketch is completed. At this point, the current edit task assumes control and performs an operation typically
involving the geometry cached in the IEditSketch::Geometry property.
An edit task performs an operation generally using a geometry created by the sketch tools; the task
is activated by the Editor, not by user interaction.
Having the edit sketch geometry creation and management abstracted away from the edit tasks allows the sketch
tools to be reused with any number of edit tasks; however, edit tasks can indirectly exhibit some control on the sketch
tools. The current edit task sets the geometry type of the edit sketchfor example, polygon, polyline, multipoint, or
pointand this ultimately affects whether the sketch tools are enabled. For example, the Create New Feature task
determines the geometry type of the target layer (esriGeometryType::esriGeometryPolygon) and sets the edit sketch
geometry type to the same type. If an edit tasks enabled conditions are not met, it can set the edit sketch geometry
type to esriGeometryType::esriGeometryNull, which indicates to all the sketch tools that they should not be enabled.
For example, the Reshape Feature edit task indirectly disables the sketch tools whenever the feature selection count
does not equal one.
The current edit task can affect the status of the sketch tools.
Most edit tasks listen to events fired by the Editor object to maintain the edit session. For example, most tasks listen
to the OnCurrentLayerChanged event to control the edit sketch geometry type including setting it to null to disable the

298

sketch tools. A few tasks respond to OnSketchModified to automatically complete an edit sketch when a certain set of
criteria has been met. Finally, a few listen for OnCurrentLayerChanged to delete the edit sketch in case the new target
layer is incompatible with the current geometry type.
Deciding whether a custom edit task is the solution to your editing problem usually boils down to whether or not an
input geometry is necessary to complete a desired editing operation. The geometry does not have to be one created
using the sketch tools; the geometry could come from almost anywhere, such as a selected feature. The Modify
Feature task is an example of this.
A custom edit task may be a suitable solution if you need to use a geometry to complete the desired
editing operation.
All edit tasks must be registered in the ESRI Edit Tasks component category. By default, custom edit tasks appear
under the 'Other Tasks' group; you can use the editor options dialog box to better organize them.

For an example of an edit task that creates a new point feature based on the existing sketch geometry, see the
ConstructPoint Edit Task Example. Other examples of edit tasks you may want to create include:

A measuring task, which can be used to construct a complex polygon or polyline, reports back the final length.

A task that allows you to create new polylines but additionally intersects all polylines the new one crosses.

See Also Extending the Editing Framework and ConstructPoint Edit Task Example.

Construct Point Edit Task Example

Description This project provides a custom edit task, an edit task that can be used to create new point features at
the end of a polyline edit sketch using any of the available sketch tools, without having to first construct a temporary
geometry to help locate the new point feature. The task will appear in the Tasks combo box on the Editor toolbar.

299

Design Coclass ConstructPointTask is a subtype of the EditTask abstract class and also sinks the IEditEvents interface.
License required ArcEditor or above
Libraries Carto, Display, Editor, Geodatabase, and Geometry
Languages Visual Basic
Categories ESRI Edit Tasks
Interfaces IEditTask and IEditEvents
How to use
1.

Register the ConstructPointTaskVB.dll and double-click the ConstructPointVB.reg file to register to component
categories.

2.

Open ArcMap, add data containing polylines, polygons, and at least one point layer. The data must be editable.

3.

Start an edit session.

4.

Choose the Create point at end of sketch edit task (which will be listed under Other Tasks), and make sure the
target layer contains points.

5.

Using any of the edit tools, create a new edit sketch that terminates at the location you want to create your
new point feature. For example, you could edit a polyline layer and use the Direction and Length commands
from the Edit Sketch context menu to create a point at a certain distance and direction from an existing point.

6.

Using any of the edit tools, create a new polyline edit sketch which terminates at the location you want to
create your new point feature. For example, you could edit a polyline layer and use the Direction and Length
commands from the Edit Sketch context menu to create a point at a certain distance and direction from an
existing point.

7.

Finish the edit sketch.


A new point feature will be added to your target layer at the last vertex of the edit sketch.

The case for a construct point edit task


You want to be able to create new point features at precise locations relative to existing features. For example, you
want to add a new Pole feature 145 feet from an existing pole, at a bearing of 40 degrees. Using the existing tool, you
could create a temporary line feature starting at the existing pole and ending at the required location by using the
Direction and Length command; then you could create the new Pole point feature at the end of the temporary line and
delete the line.

The best solution for this scenario is to create a custom edit task that creates new point features at the end of a
polyline edit sketch. When a polyline edit sketch has been completed, a new point feature will be added to the target
layer at the location of the end of the edit sketch, and the remaining portion of the edit sketch geometry will be
deleted.
A different way to solve this problem may be to create a custom edit tool. However, if you create an edit task, your
code can take advantage of all the sketch tools, which already exist to locate the point, lending more flexibility to your
solution.
This example demonstrates how to create a custom edit task to create point features at the end of
an edit sketch, without needing to create a temporary feature as an interim step.

Creating an edit task

By reviewing the Editor object model diagram, you will see the EditTask abstract class, which implements the
IEditTask interface. As mentioned in About Edit Tasks, most edit tasks also listen to events from the Editor.

300

Creating the ConstructPointTask

To meet the requirements described above, you will create a subtype of the EditTask abstract class by implementing
IEditTask, and you will also sink the IEditEvents interface.
Implementing IEditTask
The first member of IEditTask to be called will be Activate, when the task is selected in the editor toolbar. Store a
reference to the Editor object, which is passed in to this member; you will need to use the IEditSketch and IEditLayer
interfaces of this member.
[Visual Basic 6]

Private m_pEditor As esriEditor.IEditor


Private m_pEditSketch As esriEditor.IEditSketch
Private m_pEditLayers As esriEditor.IEditLayer
Private Sub IEditTask_Activate(ByVal Editor As esriEditor.IEditor, ByVal oldTask As
esriEditor.IEditTask)
Set m_pEditor = Editor
Set m_pEditSketch = m_pEditor 'QI
Set m_pEditLayers = m_pEditor 'QI
End Sub
When the user finishes an edit sketch with the ConstructPointTask as the active task, the OnFinishSketch method will
be called. In this method, you need to retrieve the final point of the sketch geometry and create a new feature. The
new feature also needs any default values set, and finally, the map display should be refreshed so the new feature and
new selection appear correctly.
1.

Start a new edit operation.


[Visual Basic 6]

Private Sub IEditTask_OnFinishSketch()


Dim bInOperation As Boolean
m_pEditor.StartOperation
bInOperation = True
2.

Find the last point in the edit sketch.


[Visual Basic 6]

Dim pPoint As esriGeometry.IPoint


Set pPoint = m_pEditSketch.LastPoint
3.

Create a new feature, and set the default values and subtype of the feature if necessary (not all FeatureClasses
support ISubtypes).
[Visual Basic 6]

Dim pFeature As esriGeodatabase.IFeature


Set pFeature = m_pEditLayers.CurrentLayer.FeatureClass.CreateFeature
Dim pRowSubTypes As esriGeodatabase.IRowSubtypes
Dim pSubtypes As esriGeodatabase.ISubtypes
If TypeOf m_pEditLayers.CurrentLayer.FeatureClass Is esriGeodatabase.ISubtypes Then
Set pSubtypes = m_pEditLayers.CurrentLayer.FeatureClass
Set pRowSubTypes = pFeature
If pSubtypes.HasSubtype Then
pRowSubTypes.SubtypeCode = m_pEditLayers.CurrentSubtype
End If
pRowSubTypes.InitDefaultValues
End If
4.

Set the geometry of the new point feature from the sketch LastPoint and store the new feature. At this point you
can also stop the edit operation.
[Visual Basic 6]

Set pFeature.Shape = pPoint


pFeature.Store
m_pEditor.StopOperation "Add Point"
bInOperation = False

301

5.

Invalidate the area around the new feature.


[Visual Basic 6]

Dim pInvalidArea As esriGeodatabase.IInvalidArea


Set pInvalidArea = New esriCarto.InvalidArea
Set pInvalidArea.Display = m_pEditor.Display
pInvalidArea.Add pFeature
pInvalidArea.Invalidate esriAllScreenCaches
6.

Refresh map according to old and new selections.


[Visual Basic 6]

Dim pActiveView As esriCarto.IActiveView


Set pActiveView = m_pEditor.Map
pActiveView.PartialRefresh esriViewGeoSelection, Nothing, Nothing
m_pEditor.Map.ClearSelection 'Clear the selection
m_pEditor.Map.SelectFeature m_pEditLayers.CurrentLayer, pFeature
pActiveView.PartialRefresh esriViewGeoSelection, Nothing, Nothing
7.

You should add an error handler to OnMouseDown to stop any edit operation if there is a problem during the
function.
[Visual Basic 6]

Private Sub IEditTask_OnFinishSketch()


On Error GoTo ErrorHandler:
...
Exit Sub ' Exit sub to avoid error handler
ErrorHandler:
If bInOperation Then m_pEditor.AbortOperation
MsgBox "Failed to construct new point at end of sketch."
End Sub
From IEditTask::Name return the string that you would like to appear in the edit tasks combo box to identify the task.
[Visual Basic 6]

Private Property Get IEditTask_Name() As String


IEditTask_Name = "Create Point at End of Sketch"
End Propert
Implementing IEditEvents
Because this edit task deals strictly with creating new point features, the task should disable the sketch tools
whenever the target layer is set to something other than a point feature layer. To do this, you can listen to the
OnCurrentLayerChanged event of the IEditEvents interface.
1.

Begin by sinking the default interface of the Editor coclass, IEditEvents, in your class.
[Visual Basic 6]

Private WithEvents EditorEvents As esriEditor.Editor


2.

Add code to the IEditTask::Activate method to start listening to the editor events. Add a line to call the
OnCurrentLayerChanged method straightaway. You will add this member in the next step.
[Visual Basic 6]

Private Sub IEditTask_Activate(ByVal Editor As esriEditor.IEditor, ByVal oldTask As


esriEditor.IEditTask)
Set m_pEditor = Editor
Set m_pEditSketch = m_pEditor ' QI
Set m_pEditLayers = m_pEditor ' QI
Set EditorEvents = m_pEditor
' Call OnCurrentLayerChanged to see if sketch tools should be active or
not
EditorEvents_OnCurrentLayerChanged
End Sub
3.

Then implement the OnCurrentLayerChanged member of IEditEvents. Check that the target layer is a point
layer, and if it is not, set the edit sketch geometry type to null; this will disable the sketch tools until a different
layer or task is selected is selected. If a point layer is selected, then set the edit sketch to contain a Polyline..
[Visual Basic 6]

Private Sub EditorEvents_OnCurrentLayerChanged()


' Exit if there is no target (current) layer
If m_pEditLayers.CurrentLayer Is Nothing Then Exit Sub

302

If Not m_pEditLayers.CurrentLayer.FeatureClass.ShapeType = esriGeometryPoint Then


m_pEditSketch.GeometryType = esriGeometryNull
Else
m_pEditSketch.GeometryType = esriGeometryPolyline
End If
End Sub
4.

When the task is deactivated, your edit task should stop listening for events, as you should only respond to
events when your task is the active task.
[Visual Basic 6]

Private Sub IEditTask_Deactivate()


Set EditorEvents = Nothing
End Sub
Now you can compile the ConstructPointTask project and use it in an ArcMap edit session. Below you can see the task
being used to create a feature in conjunction with the Distance and Direction.

See Also Extending the Editing Framework and About Edit Tasks.

About Editor Extensions


It would be easy to say that editor extensions extend the editing framework, and although this is true, so do custom
commands, tools, edit tasks, and so on. An editor extension is similar to any other type of extension. When the editor
loads its extensions, it passes an IEditor reference to each extension using the variant initializationData parameter in
the IExtension::Startup routine; whereas ArcMap extensions receive an IApplication reference, as do commands and
tools. Editor extensions are another way developers can plug in to the editing model and extend it. The difference
between editor extensions and other editor customizations is that extensions are automatically loaded and unloaded
by the application. There can be only one instance of an extension running at a time.
Editor extensions are similar to standard extensions, but they receive a reference to the Editor object
instead of the Application.
Editor extensions must be registered in the ESRI Editor Extensions component category. Like standard extensions, one
instance of each of the classes in this category is instantiated when ArcMap starts up.
There are two general cases where a custom editor extension is appropriate: new low-level components or state
management and automatic response to events.
Low-Level Components
Editor extensions are appropriate when you have a collection of commands and/or tools that need to access some
common data or functionality. For example, a custom editing application may have several editing properties, such as
the snap tolerance, that several related tools rely on. More often than not, these related tools rely on functionality
additionally exposed by the extension. The extension is the engine for several user interface components.
Editor extensions are beneficial for many reasons:

They are automatically created and destroyed by the application.

The same extension persists throughout the entire life of the application, which helps you to manage state.

There is a standard, easy method for obtaining a reference to any extension (IEditor::FindExtension).

And finally, each extension is handed a reference to the Editor object; from there you can access almost any
part of the application.
One use of editor extensions is to tie together a number of other editor components, such as tools
and commands, allowing them to share information.

Standalone components would not share many of these benefits; you would need to consider such questions as how
would the component be created, how would it be shared, and how would it hook into the application? Extensions
completely solve these problems.

303

The editing framework ships with several editing extensions of this type including the Attributes Window, the Digitizer
extension, the Topology Editor extension, and the Conflict Resolution extension.
Automatic Response
Custom editor extensions can automatically respond to specific events fired within an edit session, typically for
validation purposes. Such extensions would sink the IEditEvents, IEditEvents2 or IEditEvents3 outbound interfaces on
the Editor object. This is by far the more common type of custom editor extension created by users. For example, a
custom editor extension may automatically validate features whenever they are created or modified.
Another common use of editor extensions is to trigger some code in response to editor events.
For an example of an editor extension that performs both of these functions, see the Subtypes Snap Agent Example.
See Also Extending the Editing Framework and Subtypes Snap Agent Example.

About Snap Agents


Snap agents facilitate geometry placement. For example, the sketch tools make use of snap agents to enable a user to
precisely place an edit sketch vertex. Because snap agents are so widely used, the editing framework manages a
snapping environment, which is a collection of snap agents and a snap tolerance (ISnapEnvironment::SnapTolerance).
Snap agents help you place sketch geometries during editing.
Tools that make use of the editing snap environment usually do so in their ITool::OnMouseMove implementation. In
this event, tools create a point based on their current location and pass this point to the snap environment which,
acting essentially as a black box, either modifies the point's x,y coordinates or does not. The original point is passed
into the snap environment engine via a call to ISnapEnvironment::SnapPoint, which takes the point as a parameter.
SnapPoint in turn calls each snap agents ISnapAgent::Snap method until one of them returns True. A True response
indicates that the snap agent has found a new point that meets its unique snapping criteria; in this case, the original
points coordinates are modified to reflect that of the snap point. The first snap agent to return True determines the
snapping behavior; no other snap agents are called after this point.
A special type of snap agent is the feature snap agent. Like its name suggests, a feature snap agent snaps to features
in the edit session. The snapping environment automatically generates one feature snap agent for every feature class
in the edit session. Feature snap agents have two properties that govern their behavior:

The FeatureClass, which controls the feature class in the edit session it will attempt to snap to.

The HitType, which controls the part of the feature to snap to, for example, boundary, endpoint, and vertex.

All other snap agents are generally referred to as regular snap agents. ArcMap ships with three of these:

Perpendicular to sketch

Edit sketch vertices

Edit sketch edges


There are two types of snap agents: those that snap to existing features and those that snap to an
element of the sketch geometry.

Below you can see the Snapping Environment dockable window. The feature snap agents are listed in the top half of
the window, and the regular snap agents are in the bottom half.

It is possible to control the order of the nonfeature snap agents (the regular snap agents) in the snap environment
dialog box on a per-user basis by editing the snpdlg.ini file. This .ini file can be found in the Application Data directory
in the user's profile, for example, C:\Documents and Settings\Steve\Application Data\ESRI\ArcMap\Editor. The file
contains a list of snap agent GUIDs, and the order of these GUIDs controls the order in the dialog box. Note that the
.ini file does not override the category settings; for example, the second GUID in the list will be the first snap agent

304

listed in a particular category if its category differs from the snap agent listed above it.
If a desired snap agent does not exist, you can extend the system by creating your own and adding it to the snap
environment. Create a custom snap agent by implementing ISnapAgent and IPersistVariant. Note, C++ developers
should implement IPersistStream instead of IPersistVariant. Unfortunately, you cannot create a custom feature snap
agent; only regular snap agents can be implemented. You could, of course, create a snap agent that behaves exactly
like a feature snap agent, but you won't be able to have it appear in the top half on the snapping window. Instead, you
will have to create your own control to set its target feature class and hit type.
For an example of a snap agent, see the Subtypes Snap Agent Example. Other examples of edit tasks you may want
to create include:

Snap to center of polygons

Snap to center of circular arcs

Snap to buffer

See Also Extending the Editing Framework and Subtypes Snap Agent Example.

Subtypes Snap Agent Example


Object Model Diagram

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
1.

Register the SnapAgentVB.dll and double-click the SnapAgentVB.reg file to register to component categories.

2.

Open ArcMap, click Tools, then click Customize.

3.

In the Customize dialog box, click the Commands tab, then click on 'Extending ArcObjects' in the left-hand
Categories list.

4.

In the Commands list, choose the SubTypesSnap Dockable Window command, and drag this onto the Editor
toolbar below the Snapping command. Click Close to dismiss the Customize dialog box.

5.

Add data from a geodatabase to ArcMapat least one of your data layers should contain a number of subtypes.

6.

Click Editor and click Start Editing.

7.

Click Editor again, and click Snapping to display the Snapping Environment Dockable Window. You should see
the Sub Types snap agent listed under Miscellaneous in the lower half of the window.

8.

Click Editor once again, and click the custom SupTypesSnap Dockable Window command to show the Extended
Snap Agents dockable window.

305

9.

You can now perform edits using the custom snap agent, and snap only to those subtypes selected in the
Extended Snap Agents window.

The case for a subtypes snap agent


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.

Creating a Snap Agent

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.

306

Creating the SubtypesSnap Agent

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.
Creating and Implementing ISubtypesSnapAgent
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.
[Visual Basic 6]

Private m_pFeatureClass As esriGeoDatabase.IFeatureClass ' The snap FeatureClass


Private m_lCode As Long

' The snap Subtype code

Private m_sName As String

' The snap agent name

Private m_bIsOn As Boolean

' Is the agent active?

Private Property Get ISubtypesSnapAgent_FeatureClass() As esriGeoDatabase.IFeatureClass


Set ISubtypesSnapAgent_FeatureClass = m_pFeatureClass
End Property
Private Property Set ISubtypesSnapAgent_FeatureClass(RHS As esriGeoDatabase.IFeatureClass)
Set m_pFeatureClass = RHS
End Propert
Implementing ISnapAgent
The Name property should return the string you want to be displayed in the Snapping Environment dockable window.
[Visual Basic 6]

Private Property Get ISnapAgent_Name() As String


ISnapAgent_Name = "Sub Types"
End Propert
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

307

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.
[Visual Basic 6]

Private m_FeatureCache As esriCarto.IFeatureCache


...
Private Sub FillCache(FClass As IFeatureClass, pPoint As IPoint, Distance As Double)
m_FeatureCache.Initialize pPoint, Distance
m_FeatureCache.AddFeatures FClass
End 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.
1.

First, check that the snap agent is turned on, and a feature class has been set. If you exit the function without
having performed a snap, return False.
[Visual Basic 6]

If Not m_bIsOn Then


ISnapAgent_Snap = False
Exit Function
End If
If m_pFeatureClass Is Nothing Then
ISnapAgent_Snap = False
Exit Function
End If
2.

Check that the feature cache is full, and if the feature falls outside the extent of the feature cache, refill the
cache centered on the new Point. The example code uses a distance of ten times the tolerance distance,
Tolerance, which is passed in as a parameter to the Snap member.
[Visual Basic 6]

Dim dMinDist As Double


dMinDist = Tolerance * 10
If m_FeatureCache Is Nothing Then
Set m_FeatureCache = New FeatureCache
End If
If Not m_FeatureCache.Contains(Point) Then
FillCache m_pFeatureClass, Point, dMinDist
End If
3.

Now you can perform the main test of the Snap method, which is to work out which is the closest vertex on the
cached features to the Point variable (which is passed in as a parameter to the Snap member). The important
elements of the code below are:

o
o
o
o
o

Loop through all of the features in the cache.


Ignore features with the wrong Subtype code.
Use the HitTest method to work out which is the closed point on the feature to the input Point.
If the HitPoint is closer than the current minimum distance, save the HitPoint, set the new minimum
distance value, and set bHasSnapped to indicate that a snap has taken place.
The hit test is performed using the esriGeometryPartBoundary esriGeometryHitPartType constant,
indicating that the sketch will snap to the closest point on anywhere on the boundary of a polygon feature
or anywhere along a polyline feature, not necessarily to a vertex of the feature.

[Visual Basic 6]

' pHitPoint will be used in the For loop below.


Dim pHitPoint As IPoint
Set pHitPoint = New Point
' Loop thru all of the features
Dim pFeature As IFeature
Dim pRowSubtypes As IRowSubtypes
Dim pHitTest As IHitTest

308

Dim bHasSnapped As Boolean


Dim lPartIndex As Long, lSegmentIndex As Long, bRightSide As Boolean
Dim dDist As Double, dX As Double, dY As Double
Dim count As Integer
For count = 0 To m_FeatureCache.count - 1
Set pFeature = m_FeatureCache.Feature(count)
Set pRowSubtypes = pFeature 'QI
' Only interrogate features that match subtype code
If pRowSubtypes.SubtypeCode = m_lCode Then
Set pHitTest = pFeature.Shape
If (pHitTest.HitTest(Point, Tolerance, esriGeometryPartBoundary, pHitPoint, dDist,
lPartIndex, lSegmentIndex, bRightSide)) Then
If dDist < dMinDist Then
pHitPoint.QueryCoords dX, dY
dMinDist = dDist
bHasSnapped = True
End If
End If
End If
Next count
4.

Perform a last check to make sure the hit distance, minDist, is within the search tolerance.
[Visual Basic 6]

If dMinDist >= Tolerance Then


ISnapAgent_Snap = False
Exit Function
End If
5.

If the bHasSnapped variable indicates that the code found a snap point, modify the coordinates of the Point
parameter, which was passed in to the Snap function to reflect those of the snap point you found in the loop,
and return true.
[Visual Basic 6]

If bHasSnapped Then
Point.PutCoords dX, dY
ISnapAgent_Snap = True
End If
Implementing ISnapAgentFeedback
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.
[Visual Basic 6]

If dDist < dMinDist Then


pHitPoint.QueryCoords dX, dY
dMinDist = dDist
bHasSnapped = True
m_sSnapText = "OID:" & pFeature.OID & "; Part:" & lPartIndex & "; Segment:"
& lSegmentIndex
End If
...
Private Property Get ISnapAgentFeedback_SnapText() As String
ISnapAgentFeedback_SnapText = m_sSnapText
End Property
Implementing IPersistStream/IPersistVariant
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.
[Visual Basic 6]

Private Sub IPersistVariant_Save(ByVal Stream As esriSystem.IVariantStream)


Stream.Write c_PersistVersion

309

Stream.Write m_bIsOn
If m_pFeatureClass Is Nothing Then
Stream.Write False
Else
Stream.Write True
Dim pDataset As esriGeoDatabase.IDataset
Set pDataset = m_pFeatureClass
Stream.Write pDataset.FullName
Stream.Write m_sName
Stream.Write m_lCode
End 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.
[Visual Basic 6]

Private Sub IPersistVariant_Load(ByVal Stream As esriSystem.IVariantStream)


...
Dim hasFeatClass As Boolean
hasFeatClass = Stream.Read
If hasFeatClass Then
Dim pName As esriSystem.IName
Set pName = Stream.Read
m_sName = Stream.Read
m_lCode = Stream.Read
Set m_pFeatureClass = pName.Open
End If
End Sub
The SubtypesSnap agent should be registered to the ESRI Snap Agents component category.

Plugging the SubtypesSnap agent into ArcMap


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.

Creating an Editor Extension

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.

Creating the SnapExtension


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.

310

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.
The Snap Form
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.

Implementing IExtension
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.
[Visual Basic 6]

Private Sub IExtension_Startup(initializationData As Variant)


If initializationData Is Nothing Then Exit Sub
If Not TypeOf initializationData Is IEditor Then Exit Sub
Set m_pEditor = initializationData
Set m_pSnapEnv = m_pEditor
Set EditorEvents = 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.)
[Visual Basic 6]

Set m_snapForm = New SnapAgentVB.SnapForm


Load m_snapForm
Set ListBoxEvents = m_snapForm.List1
Dim pUID As New esriSystem.UID
pUID.Value = "SnapAgentVB.SnapDockableWindow"
Dim pDockWinMgr As esriFramework.IDockableWindowManager
Set pDockWinMgr = m_pEditor.Parent
Set m_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.
Implementing IEditEvents
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.
[Visual Basic 6]

Private Sub EditorEvents_OnStartEditing()


' Don't bother looking for subtypes if the workspace is file based
If Not m_pEditor.EditWorkspace.Type = esriFileSystemWorkspace Then
' Create an Array object that will locally manage the snap agents
Set m_pSnapAgentArray = New esriSystem.Array
SetUp

311

End 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:
1.

Determine which feature classes in the edit session workspace are actually in the edit session.

2.

Find all the feature classes in the Map that have subtypes.
[Visual Basic 6]

If TypeOf pFeatureClass Is esriGeoDatabase.ISubtypes Then


Set pSubtypes = pFeatureClass
If pSubtypes.HasSubtype Then
pMySet.Add pFeatureClass
End If
End If
For each feature class, enumerate the subtypes.

3.

[Visual Basic 6]

Set pEnumSubtypes = pSubtypes.Subtypes


pEnumSubtypes.Reset
newSubtypeName = pEnumSubtypes.Next(newSubtypeCode
4.

For each subtype found, check that a snap agent does not already exist for that subtypea snap agent
may have been saved in the document or an edit session which was started, stopped, and restarted. Do
this by iterating all the snap agents in the snapping environment, m_pSnapEnv, looking for snap agents
that implement ISubtypesSnapAgent and have a matching SubtypeName.

5.

Create a new SubtypesSnap agent for each subtype found, and set its FeatureClass, SubtypeCode, and
SubtypeName.
[Visual Basic 6]

Set newSnapAgent = New SnapAgentVB.SubtypesSnap


Set newSnapAgent.FeatureClass = pSubtypes
newSnapAgent.SubtypeCode = newSubtypeCode
newSnapAgent.SubtypeName = newSubtypeNam
Add each agent to the Editor's snapping environment.

6.

[Visual Basic 6]

m_pSnapEnv.AddSnapAgent newSnapAgen
For each agent, add an item to the list box on the Snap Form, indicating if the snapAgent is selected or
not.

7.

[Visual Basic 6]

m_snapForm.List1.AddItem newSnapAgent.FeatureClass.AliasName + vbTab +


newSnapAgent.SubtypeName
If newSnapAgent.IsOn Then
m_snapForm.List1.Selected(m_snapForm.List1.ListCount - 1) = True
End If
Add a reference to each new SubtypesSnap agent to an array stored as a member variable of the
extension. You will use this in the following section.

8.

[Visual Basic 6]

Private m_pSnapAgentArray As esriSystem.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.
[Visual Basic 6]

Private Sub EditorEvents_OnStopEditing(ByVal Save As Boolean)


If Not Save Then
If Not m_snapForm Is Nothing Then
m_snapForm.List1.Clear
End If
If m_pDockWin.IsVisible Then
m_pDockWin.Show False
End If

312

End If
End Sub
Listening to ListBoxEvents
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.
[Visual Basic 6]

Private Sub ListBoxEvents_ItemCheck(Item As Integer)


Dim pSubtypesSnapAgent As SnapAgentVB.ISubtypesSnapAgent
Set pSubtypesSnapAgent = m_pSnapAgentArray.Element(Item)
If pSubtypesSnapAgent Is Nothing Then Exit Sub
If m_snapForm.List1.Selected(Item) = True Then
pSubtypesSnapAgent.IsOn = True
Else
pSubtypesSnapAgent.IsOn = False
End If
End Sub
Creating and Implementing ISubtypesSnapExtension
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.
[Visual Basic 6]

Private Property Get ISubtypesSnapExtension_IsDialogVisible() As Boolean


If m_pDockWin Is Nothing Then Exit Property
ISubtypesSnapExtension_IsDialogVisible = m_pDockWin.IsVisible
End Property
Private Sub ISubtypesSnapExtension_ToggleDialogVisibility()
If m_pDockWin Is Nothing Then Exit Sub
m_pDockWin.Show Not m_pDockWin.IsVisible
End Sub
Private Property Get ISubtypesSnapExtension_SnapDialogHWnd() As Long
If m_snapForm Is Nothing Then Exit Property
ISubtypesSnapExtension_SnapDialogHWnd = m_snapForm.List1.hWnd
End 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.

313

Creating the SnapDockableWindow

To provide a mechanism for users to turn each SubtypeSnap agent on and off, create a subtype of the
DockableWindow abstract class called SnapDockableWindow.
Implementing IDockableWindowDef
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.
[Visual Basic 6]

Private m_snapExt As SnapAgentVB.ISubtypesSnapExtension


...
Private Sub IDockableWindowDef_OnCreate(ByVal hook As Object)
Dim pApp As esriFramework.IApplication
Set pApp = hook
Dim pUID As New esriSystem.UID
pUID = "esriEditor.Editor"
Dim pEditor As esriEditor.IEditor
Set pEditor = pApp.FindExtensionByCLSID(pUID)
pUID = "SnapAgentVB.SnapExtension"
Set m_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
[Visual Basic 6]

Private Property Get IDockableWindowDef_ChildHWND() As esriSystem.OLE_HANDLE


If m_snapExt Is Nothing Then Exit Sub
IDockableWindowDef_ChildHWND = m_snapExt.SnapDialogHWnd
End Property
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.

Creating the ShowSnapWindow command

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

314

IDockableWindowDef::OnCreate).
[Visual Basic 6]

Private m_snapExt As SnapAgentVB.ISubtypesSnapExtension


Private m_pEditor As esriEditor.IEditor
...
Private Sub ICommand_OnCreate(ByVal hook As Object)
If Not TypeOf hook Is esriArcMapUI.IMxApplication Then Exit Sub
Dim pApp As esriFramework.IApplication
Set pApp = hook
Dim pDockWinMgr As esriFramework.IDockableWindowManager
Set pDockWinMgr = hook

' QI for IDockableWindowManager

Dim pUID As New esriSystem.UID


pUID = "esriEditor.Editor"
Set m_pEditor = pApp.FindExtensionByCLSID(pUID)
pUID = "SnapAgentVB.SnapExtension"
Set m_snapExt = m_pEditor.FindExtension(pUID)
End Sub
When the command is clicked, change the visibility of the SnapDockableWindow by using the
ISubtypesSnapExtension::ToggleDialogVisibility method.
[Visual Basic 6]

Private Sub ICommand_OnClick()


m_snapExt.ToggleDialogVisibility
End Sub
In the Checked property, indicate the current state of the dockable window.
[Visual Basic 6]

Private Property Get ICommand_Checked() As Boolean


ICommand_Checked = m_snapExt.IsDialogVisible
End Property
Return the Enabled state based on the EditState of the Editor.
[Visual Basic 6]

Private Property Get ICommand_Enabled() As Boolean


ICommand_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.

315

See Also Extending the Editing Framework, About Snap Agents, and About Editor Extensions.

About Custom feature inspectors


The ArcMap Attributes dialog box contains two panels: the left panel lists the features in the focus map that are
selected and editable, and the right panel houses a feature inspector. ArcMap ships with a standard feature inspector,
which enables attribute editing.

The Attributes dialog box can be customized by replacing the right panel with your own custom
feature inspector.
For any feature class that resides in a geodatabase, you can replace the default feature inspector with a custom
feature inspector. Custom feature inspectors can be assigned to specific feature classes. Selecting a feature in the left
panel of the Attributes dialog box activates the associated feature inspector in the right panel.
Because feature inspectors are implemented on a feature class extension, you can only apply a custom feature
inspector to geodatabase feature classes, not shapefiles or coverages.
Remember, after you have implemented a custom feature inspector, a user can still choose to edit attributes in the
standard way by using the table window of the feature class.
A common reason for implementing a custom feature inspector is to provide a more controlled editing experience for
particular feature classes. With specific knowledge of the business data, you can provide sophisticated user interface
facilities to enable better editing; for example, you might prefer to use a calendar control to enter dates.
The alternative to a custom feature inspector is to implement an editing form outside the context of the Attributes
dialog box. The advantage here is that you have more complete control over the user interface, since you are not
restricted by the interaction with the left panel of the standard dialog box. The disadvantage is that you would be
presenting users with a more mixed environment. The Attributes dialog box caters to all feature classes from a single
map selection event, but implementing a separate dialog box would complicate the user interface with alternative
methods of editing and interaction with map selections. In this case, you may prefer to reimplement the entire
Attributes dialog box; for an example of this see the 'Feature Inspector' sample in the ArcGIS Developer Help.
Note: The terms feature inspector and object inspector are often used interchangeably. You cannot use the Attributes
dialog box for inspecting nonspatial objects, so feature inspector is the most appropriate usage.

Tabbed Feature Inspector Example

316

Description This project provides a custom feature inspector that shows the standard feature inspector in one tab and
on a second tab, if appropriate, an image corresponding to the feature.
Design Subtype of FeatureClassExtension abstract class.
License required ArcEditor
Libraries Editor, Geodatabase, and System
Languages Visual Basic
Categories ESRI GeoObject ClassExtensions
Interfaces IClassExtension, IFeatureClassExtension, IObjectInspector, and IObjectClassEvents.
How to use
1.

Register TabbedInspectorVB.dll, and double-click the TabbedInspectorVB.reg file to register to component


categories.

2.

Open ArcMap and add the WildlifeSightings feature class (from the FeatureInspector feature dataset) from the
ExtendingArcObjects.mdb personal geodatabase that is installed in the Data folder of the Developer Kit Samples.
This feature class is preconfigured with the class extension implementing the custom feature inspector.

3.

Start editing, select some of the wildlife points and open the Attributes dialog box to inspect the tabbed display.

The case for a tabbed feature inspector


Imagine a feature class that has a Photo attribute, which is a geodatabase raster field containing an image of the
feature. When editing the feature class, you can view the image by clicking the Photo field in the Feature Inspector
and clicking the button, which should then be displayed by the field name.

Your users may find that this is not the most convenient way of viewing the raster. They would prefer to see the
image, if present, shown on another tab in the Attributes dialog box, rather than via the standard button on the dialog
box.
In this example, the right panel of the Attributes dialog box is customized to have a tab that displays
the image, rather than the image being available via the standard button within the right pane of the
Attributes dialog box.
Creating a subtype of ObjectClassExtension and ObjectInspector

By reviewing the Editor object model diagram, you can see that the existing FeatureInspector and DimensionInspector
classes inherit from the ObjectInspector abstract class and implement the IObjectInspector interface.

317

A custom feature inspector is a special kind of object class extension. To create a custom feature inspector, you must
create a subtype of the abstract class ObjectClassExtension by implementing the IClassExtension and
IObjectClassExtension interfaces. You can find the ObjectClassExtension abstract class on the Geodatabase object
model diagram.
Class extensions and their deployment are described in Chapter 7, 'Customizing the geodatabase', so this section will
concentrate on aspects relevant to feature inspectors only.

Creating a TabbedFeatureInspector

To solve the requirements of this example, you will create a subtype of ObjectClassExtension by implementing
IClassExtension and IObjectClassExtension.
To provide the inspector functionality, you will also implement IObjectInspector. To respond to changes in the feature
being edited, you will also sink events from the IObjectClassEvents interface.
Add to your project a Form with a tab control which has two tabs. One tab will contain the default feature inspector
call this tab Attributes, and add a picture box to fill the tab. Call the other tab Image, and add a MapControl to fill the
tabthis will be used to display the raster appropriate to a feature.

Note that you do not always need to include the default inspector; you may create a simpler component that shows
just a plain untabbed form.
Implementing IObjectInspector
A feature inspector is contained in the right panel of the Attributes dialog box, so you need inform that dialog box of
what you would like to display inside it. This is done by supplying the IObjectInspector::hWnd property with the
handle of your form. Instead of passing the handle of the form, the example passes the handle of a PictureBox control
on which the rest of the form objects are placed. It would be ideal to pass the window handle of the tab control, but in
this case the SSTab control does not have a Resize event which, as will be explained later, is necessary for this
example.
[Visual Basic 6]

318

Private Property Get IObjectInspector_hWnd() As OLE_HANDLE


...
IObjectInspector_hWnd = frmInspector.picWhole.hwnd
End Property

The default feature inspector is made by cocreating from the FeatureInspector class. It is best to do this initialization
in the hWnd propertyplacing this code in Class_Initialize or IClassExtension::Init would be inefficient since there are
many occasions when your feature class will be opened in a context that does not involve the Attributes dialog box.
After creating the default inspector, you must hook its window into your form at run time. This can be done by calling
two Win32 API functions: SetParent, followed by ShowWindow.
[Visual Basic 6]

Private Property Get IObjectInspector_hWnd() As OLE_HANDLE


' If form not already loaded, do initialization
If Not m_bFormLoaded Then
Load frmInspector
If m_pDefaultInspector Is Nothing Then
Set m_pDefaultInspector = New FeatureInspector
End If
' hook the default inspector's window into the form
SetParent m_pDefaultInspector.hwnd, frmInspector.picDefault.hwnd
ShowWindow m_pDefaultInspector.hwnd, SW_SHOW
' pass the default inspector to the form (needed for resizing)
frmInspector.Inspector = m_pDefaultInspector
m_bFormLoaded = True
End If
IObjectInspector_hWnd = frmInspector.picWhole.hwnd
End Property
IObjectInspector::Inspect is triggered every time the active feature in the Attributes dialog box is changed or another
feature is selected via the map. Note that an enumeration of objects is supplied as a parameter. Normally, just a
single feature will be present in the enumeration, but if the area at the top of the list in the Attributes dialog box is
selected, then all the features in the dialog box are supplied in the enumeration. Note that in the latter case the
default inspector applies edits to all the selected features at once.

If the user highlights the area at the top of the list of selected features, all the features are passed to
IObjectInspector::Inspect. Normally just the single active feature is supplied.
In the example it would be inappropriate to show the image tab when no single feature is active, so different code is
executed if there is more than one feature in the enumeration.
[Visual Basic 6]

Private Sub IObjectInspector_Inspect(ByVal Objects As esriEditor.IEnumRow, ByVal pEditor As


esriEditor.IEditor)

319

m_pDefaultInspector.Inspect Objects, pEditor


' Determine if more than one object is active in the attribute window
' If so, disable the image tab
Objects.Reset
Dim pObject As IObject
Set pObject = Objects.Next ' first object
If Not Objects.Next Is Nothing Then
Call SetImageTabVisible(False)
Else
' get value of image field, and load picture with it
If IsNull(pObject.Value(m_iImageField)) Then
Set m_pRasterValue = Nothing
Call SetImageTabVisible(False)
Else
Set m_pRasterValue = pObject.Value(m_iImageField)
Call LoadImageTab
End If
End If
End Sub
You can improve performance when there are multiple features to be updated. From IEnumRow you can QI to
IEnumIDs. After looping through the IDs you can call IFeatureClass::GetFeatures, which will retrieve the features in
one call instead of fetching them one at a time with IEnumRow::Next.
There is one more IObjectInspector method that needs consideration. Copy is triggered when the user selects the
Paste command on the context menu of the Attributes dialog box. The default feature inspector implements Copy by
performing an edit operation to copy all the values from the supplied row to the row being edited. In the example, the
request is just passed on to the default inspector.
Responding to data changes
It is possible that a user will change the photo attribute using the default inspector in the first tab. In this case the
image on the second tab should be reloaded as appropriate. The best way of catching this edit event is by
implementing IObjectClassEvents , since, like IObjectInspector, this is also a class extension interface.
The example checks that the feature inspector form is loaded to allow for edits being made when the Attributes dialog
box is not present.
[Visual Basic 6]

Private Sub IObjectClassEvents_OnChange(ByVal obj As esriGeodatabase.IObject)


If m_bFormLoaded Then
If IsNull(obj.Value(m_iImageField)) Then
Set m_pRasterValue = Nothing
Call SetImageTabVisible(False)
Else
Dim pCurValue As IRasterValue
Set pCurValue = obj.Value(m_iImageField)
If Not pCurValue Is m_pRasterValue Then
Set m_pRasterValue = pCurValue
Call LoadImageTab
End If
End If
End If
End Sub
For more information about IObjectClassEvents , see the section on class extensions in Chapter 7, 'Customizing the
geodatabase'.
Resizing the feature inspector
When the Attributes dialog box is resized by the user, the window returned by IObjectInspector::hWnd is
automatically resized. In the example, the picture box containing the other form objects is automatically resized, but
code to resize the default inspector, tabs, and image picture box is required. The default inspector window is not a
member of the Visual Basic project, so it must be resized with a Win32 API call to make it the same size as its picture
box container.
[Visual Basic 6]

Private Declare Function SetWindowPos Lib "user32" (ByVal hwnd As Long, _


ByVal hWndInsertAfter As Long, ByVal X As Long, ByVal y As Long, _
ByVal cx As Long, ByVal cy As Long, ByVal wFlags As Long) As Long
...

320

Private Sub picWhole_Resize()


...
picDefault.ScaleMode = vbPixels
lSuccess = SetWindowPos(m_pDefaultInspector.hwnd, 0, 0, 0, _
picDefault.ScaleWidth, picDefault.ScaleHeight, _
SWP_NOMOVE Or SWP_NOZORDER)
picDefault.ScaleMode = vbTwips
End Sub
Now you should be able to edit the WildlifeSightings feature class and see the rasters stored with each feature in the
Attribute dialog box.

321

Appendices
Bibliography
Object orientation

Gamma, Erich; Helm, Richard; Johnson, Ralph; and Vlissides, John. Design Patterns: Elements of Reusable
Object-Oriented Software. Reading, MA: Addison-Wesley, 1995.

COM

Box, Don. Essential COM. Reading, MA: Addison-Wesley, 1998.

Box, Don; Brown, Keith; Ewald, Tim; and Sells, Chris (Eds). Effective COM: 50 Ways to Improve Your COM and
MTS-Based Applications. Reading, MA: Addison-Wesley, 1998.

Platt, David S. Understanding COM+. Redmond, WA: Microsoft Press, 1999.

Rogerson, Dale. Inside COM: Microsoft's Component Object Model. Redmond, WA: Microsoft Press, 1997.

IDL

Gudgin, Martin. Essential IDL: Interface Design for COM. Reading, MA: Addison-Wesley, 2001.

Major, Al. COM IDL and Interface Design. Chicago, IL: Wrox Press Inc., 1999.

ATL

Grimes, Richard. ATL COM Programmer's Reference. Chicago, IL: Wrox Press Inc., 1998.

Grimes, Richard. Professional ATL COM Programming. Chicago IL: Wrox Press Inc., 1999.

Grimes, Richard; Stockton, Reilly; Stockton, Alex; and Templeman, Julian. Beginning ATL 3 COM Programming.
Chicago, IL: Wrox Press Inc., 1999.

King, Brad and Shepherd, George. Inside ATL. Redmond, WA: Microsoft Press, 1999.

Rector, Brent; and Sells, Chris. ATL Internals. Reading, MA: Addison-Wesley, 1999.

Visual C++

Lippman, Stanley. C++ Primer: Second Edition. Reading, MA: Addison-Wesley, 1991.

Lippman, Stanley. Inside the C++ Object Model. Reading, MA: Addison-Wesley, 1996.

Meyers, Scott. Effective C++: 50 Specific Ways to Improve Your Programs and Designs. Reading, MA: AddisonWesley, 1992.

Meyers, Scott. More Effective C++: 35 New Ways to Improve Your Programs and Designs. Reading, MA:
Addison-Wesley, 1996.

Shepard, George and Kruglinski, David. Inside Visual C++. Fifth Edition. Redmond, WA: Microsoft Press, 1998.

Stroustrup, Bjarne. The C++ Programming Language. Third Edition. Reading, MA: Addison-Wesley, 1997.

Visual Basic

Lewis, Thomas. VB COM. Chicago, IL: Wrox Press Inc., 1999.

Pattison, Ted. Programming Distributed Applications with COM and Microsoft Visual Basic 6.0. Redmond, WA:
Microsoft Press, 1998.

Stamatakis, William. Microsoft Visual Basic Design Patterns. Redmond, WA: Microsoft Press, 2000.

Windows API programming

Appleman, Dan and Grimes, Galen A. Visual Basic Programmers Guide to the Win32 API. Indianapolis, IN: Sams
Publishing. 1999.

Roman, Steven. Win32 API Programming with Visual Basic. Cambridge, MA: O'Reilly & Associates, 1999.

Note: MSDN (Microsoft Developer Network) is referenced throughout the Extending ArcObjects book. You can find
the current public version of MSDN at www.MSDN.com. MSDN is also available through a subscription program, more
details of which can be found on the MSDN Web site.

Editing IDL
IDL enables you to create a type library, which means that programmers in different development environments can
use your component. Type libraries also contain essential information for linking components to help systems.

322

This appendix contains supplementary information to the 'Creating type libraries with IDL' section of Chapter 2,
'Developing Objects'.

In this appendix
Editing the IDL created by OLE View for a VB component
The first section contains information for VB developers, relating to the Chapter 2 section 'Creating an external type
library for a component created in VB'. It describes changes that can be made to the IDL created using OLE View from
a component defined in a VB DLL, when creating an external type library for a component created in VB.
These changes described may be useful if you intend to make the component available to other developers, in
particular those who are working in other development environments. The edits focus on undoing the internal changes
made by the VB compiler to create a type library containing the definitions you would expect.
Defining interfaces in IDL for client neutrality
The next section contains information aimed mainly at VC++ developers creating components which may be used in
other environments, in particular VB. It includes information on how to create IDL, which is equally usable in VB and
VC++. This information may also be of use to VB developers.
IDL Standards
The last section of this appendix reviews some standards applied in ArcObjects, which you may want to conform to.
They are aimed at maximizing usability. Both VB and VC++ developers should find this information useful.

Editing the IDL created by OLE View for a VB component

Remove all the coclass definitions. IDL files can only contain coclasses definitions, not implementation, and VB is
unable to make use of these definitions. The definitions of the coclasses you eventually create in your destination
coclass will be defined in the internal type library of the destination DLL. Delete the entire coclass definition block
as shown.
[uuid(10777616-EAF6-4133-9A0D-1AD236C0F929), version(1.0)]
coclass MyClass
{
[default] interface _MyClass;
interface _IMyInterface;
};

Remove unwanted interface definitions. You will find that VB includes a default interface definition for each class
in a project.
If there are any class members on these interfaces that you require, they should be moved to an appropriate
interface and implemented on the class. Note that you will also need to remove the appropriate interface names
from the forward declare list at the beginning of the library block.

Remove the underscore ( _ ) from the front of the interface names (this prevents VB from interpreting it as a
default class interface).

Remove the [hidden] attributes from the remaining interface definitions. VB defines all interfaces as hidden by
default.

Move all definitions and so on within the library block. Ensure that any enumerations or structures are declared
at the beginning of the library block before they are referenced in interface definitions.

Change enumeration declarations to include a typename at the beginning of the typedef, or an illogical name will
be created for you.
typedef [uuid(0009AFDD-4E73-41BC-AEF0-0E178D37BD22), version(1.0)]
enum M y E n u m {
enumSpring = 1,
enumSummer = 2,
enumAutumn = 3,
enumWinter = 4
} MyEnum;

Structure names are given an alias of the structure type name preceded by 'tag' by default. Remove this tag
prefix to ensure the structure can be called by its original name.

Check the names of all parameters of interface members. Names may not be included by default for outbound
parameters, and the MIDL compiler may insert illogical names.
interface IMyInterface : IDispatch
{
[id(0x68030000), propget]
HRESULT MyValue([out, retval] long* l V a l u e );
...

If you do not wish to have dual interfaces, change interface definitions to inherit from IUnknown instead of
IDispatch. If you are inheriting from IUnknown, you should also remove the [dual] and [nonextensible]

323

attributes from the interface, as they do not apply to custom (IUnknown) interfaces.
[
odl, uuid(6B908985-A9C5-4DD2-8A1A-2E48B1E5B739),
version(1.0), oleautomation
]
interface IMyInterface : I U n k n o w n {

Change all the GUIDs for the type library, interfaces, and so on. Use guidgen.exe or the ESRI utility GUID Tool to
generate new GUIDs. Alternatively, ensure you remove all references to the original interface definition in the
class module in your project, including registry entries, which includes breaking binary compatibility to reference
the new external type library.

You can use IDL to specify helpstrings and help context ID numbers to all your interfaces, interface members,
enumerations, and structures. You can also specify help information for the library.
[
uuid(C1F492EF-8521-47F8-9AC2-F2369B8715A1), version(1.0),
helpstring("MyLibrary 1.0 Type Library"), helpcontext(0x00000005)
]
library MyProject {
...
[
uuid(6B908985-A9C5-4DD2-8A1A-2E48B1E5B739),
version(1.0),
helpstring("Interface for getting values"), helpcontext(0x00000009)
]
interface IMyInterface : IUnknown {
[propget, h e l p s t r i n g ( " G e t s t h e v a l u e " ) , h e l p c o n t e x t ( 0 x 0 0 0 0 0 0 0 a ) ]
HRESULT MyValue([out, retval] long* lValue);

If required, change the internal name of the library. By default, the name will be the same as the project
filename.
...
helpstring("MyLibrary 1.0 Type Library"), helpcontext(0x00000005)
]
library M y C h o s e n L i b r a r y N a m e {

Use of the [oleautomation] attribute


If you are creating a type library to make a system interface usable with Implements, you must not use the
[oleautomation] or [dual] attributes. Type libraries must be registered before you can add them to the Visual Basic
References dialog box, and registering a type library with the [oleautomation] attribute will overwrite information
required to remote the system interface. This will cause other applications on the system to fail. The [dual] attribute
must not be used because it implies [oleautomation].
It may be useful to specify [oleautomation] while creating the typelib to enforce correct types, but the type library
must be built without the attribute before you reference it through the Visual Basic References dialog box.
Writing and editing IDL
Keep in mind that the notes given above only give you an indication of what IDL changes would generally be made
when creating an IDL file based on a VB project. IDL is a complex language, which when mapped to different
languages may have subtle issues and effects. For more information on creating type libraries from IDL, read the
references specific to IDL listed in the Bibliography.
Defining interfaces in IDL for client neutrality
A number of issues are described below that affect components created in VC++ for use in VB or other environments,
which you may like to take into account when you write your IDL. In particular, you may encounter some of these
issues when implementing interfaces in VB.
Issues specific to the outbound interface are discussed at the end of this section.
Data types
Each parameter that is part of an interface has a specified data type. To implement an interface, a development
environment must support all the data types used in that interface. VC++, for example, supports all the data types
defined by IDL. VB, however, has some data type restrictions.
To implement an interface, a development environment must support each data type used by that
interface.
Note that if you simply need to call the members of an interface, you may often be able to substitute a parameter with
a compatible data type. For example, VB allows you to call a member defined as an (unsupported) unsigned integer by
using a (supported) signed integer, although you will not be able to pass a value outside the range of the signed
integer data type.
The following table summarizes the data types supported by some development environments. For other development
environments, check the online documentation of your environment for details of supported data types.

324

Language

Base types

Extended types

IDL

Microsoft C++

Microsoft VB

boolean

unsigned char

unsupported

byte

unsigned char

unsupported

small

char

unsupported

short

short

Integer

long

long

Long

hyper

__int64

unsupported

float

float

Single

double

double

Double

char

unsigned char

unsupported

wchar_t

wchar_t

Integer

enum

enum

Enum

Interface Pointer

Interface Pointer

Interface Ref.

VARIANT

VARIANT

Variant

BSTR

BSTR

String

VARIANT_BOOL

short (-1/0)

Boolean

Multiple interface inheritance


In COM, all interfaces inherit from IUnknown, the base interface which provides access to other interfaces on an
object.
The IDL specification allows an interface to inherit from IUnknown and one or many other interfaces. The
ISimpleLineSymbol interface, for example, inherits from ILineSymbol, which in turn inherits from IUnknown.
interface ISimpleLineSymbol : ILineSymbol
interface ILineSymbol : IUnknown
This type of inheritance is supported by VC++: a class that implements the ISimpleLineSymbol interface inherits the
ILineSymbol and IUnknown interfaces.
VB, however, only supports a single level of interface inheritance, and every interface must inherit from either
IUnknown or IDispatch. The implementation of the members of the inherited interface is done 'under the covers' by
the VB compiler. As VB does not support multiple interface inheritance, you cannot implement an interface that
inherits from another interface, for example, you cannot implement ISimpleLine Symbol.
You cannot use VB to implement an interface that inherits from another interface other than
IUnknown.
Parameter types and parameter attributes
The VB environment only allows certain types of parameters in interface members. Interfaces, including the following
types of parameters, cannot be implemented in VB.

Unsupported data types (see previous section).


[in, out] BYTE byData
[in] boolean bIsOK

User-defined data types, also known as structure declarations.


[in] RECT* initialExtent

\\ Where RECT defines a tagRECT structure

Parameters defined with only the [out] attribute, or the [lcid] attribute.
[out] double* dValue

Parameters may have the sole [in] attribute as long as they are not pointers. Parameters that are pointers
generally may not have the sole [in] attribute. The exceptions to this rule are BSTR parameters, pointers to an
interface, or pointers to a SAFEARRAY.

For example, the parameters below would not be implementable in VB, as they are pointers.
[in] double* dValue
[in] BSTR* sName

The definitions below would be implementable.


[in] double dValue //parameter is not a pointer
[in] BSTR sName

//parameter is a BSTR

[in] ILineSymbol* pLineLayer


[in] SAFEARRAY* Array

//parameter is a pointer to an interface

//pointer to a SAFEARRAY.

325

The following are supported by VB. Interfaces containing these types of parameters can be implemented in VB.

Enumerations
[in] esriSimpleLineStyle style

Pointers to interfaces

Objects passed by the IDispatch interface.

Type definitions defined as Automation data types.

Arrays, as long as they are of SAFEARRAY type and contain only the simple data types supported by VB. The
SAFEARRAY should be carefully declared in the IDL to include the data type held within the array.

[retval, out] IEnvelope** pExtent

[in] IDispatch* hook

[in] OLE_COLOR rgb

SAFEARRAY(VARIANT)* saArr
Alternatively, the SAFEARRAY may be wrapped within a variant.

Most parameters defined with the [in] or [in, out] attributes. Note that exceptions to this rule are described
above.
[in] ISymbol* pSymbol
[in, out] ISymbol** ppSymbol
If the [in] attribute is missing, VB assumes the parameter to be passed by value. If the [in, out] attribute is
used, the parameter should be either a pointer to an Automation data type or a pointer to a pointer to an
interface (a double pointer).
[out, retval] IUnknown** ppObj
[out, retval] double* dValue

Default attribute
The IUnknown interface is generally given the [default] attribute. This is because VB hides the default interface on a
class, and the IUnknown interface is not required by VB developers. Note that a class may also have a default
outbound interface. See below for more information.
Hidden attribute
Methods defined with the Hidden attribute indicate an advanced feature of an interfaceone that is not expected to be
used by the vast majority of clients. The attribute is used to prevent accidental use by the more casual programmer
(as hidden members are not shown by default in the VB Object Browser or IntelliSense) while still allowing the
member to be available to those who require it.
For instance, ICommandItem::get_Command returns the internal command.
[
hidden, propget, helpcontext(2957),
helpstring("A reference to the internal command object.")
]
HRESULT Command([out,retval] ICommand** command);
Developers are not normally expected to act as clients of ICommand directly, but since containment is used in
conjunction with ICommandItem, in certain circumstances you may find you need to get the internal command object
to QI for some private or user-defined interface. At this point, the Bitmap or Create members of the internal
command's ICommand interface could be called; however, this is not a valid action for a client and the hidden
attribute is applied to the internal command to warn of methods that require careful handling. The "hidden" attribute
precedence can be seen in a number of Microsoft and non-Microsoft published interfaces.
VB developers are free to implement interfaces with hidden members, although they must ensure they provide an
implementation for any hidden members.
Restricted attribute
The Restricted attribute should only be used on interface members, never on an entire interface, although it can also
be applied to an entire type library. Members marked as restricted cannot be accessed by macro programmers (using
for example VBA in ArcGIS, VBScript, or JavaScript) or by VB programmers. In most cases, you will find it more
appropriate to use either a private interface, not available to external clients, or the hidden attribute to indicate a
more advanced feature.
Second version (Ixx2) interfaces
The rules of COM state that an interface, once published, cannot be changed. To add more functionality or change
existing functionality, it is therefore common to add interfaces to a coclass. To supplement functionality on a specific
interface, it is conventional to name the interface as the original interface with the addition of a number at the end (for
example, ILayer, ILayer2).
To allow the second interface to be implemented in VB, it is conventional for the supplemental interface to inherit from
IUnknown and to minimize the need to QI. It is also conventional to provide all the members of the original interface.
The exception to this rule is if a member is superseded by a member on the new interface.
Member names

326

Member names cannot include the underscore ("_") character if the interface is to be implemented in VB.
Return Types
Only members with an HRESULT return type allow VB to propagate errors correctly. All ArcObjects methods are
defined with a return value of HRESULT. For members that return a parameter in VB, in the IDL for the member the
final parameter is defined with the [out, retval] attribute).
Custom, dispatch and dual interfaces
See the VC++ environment documentation in the ArcGIS Developer Help system for more information on how to
define a dual, dispatch, or custom interface in VC++. The VB compiler automatically creates dual interfaces.
Outbound interfaces
Languages, such as VB and VBA, provide support only for event calls on a dispatch interface. However, ArcObjects
interfaces are custom interfaces, that is, they are based on IUnknown rather than IDispatch. Therefore, you cannot
implement existing ArcObjects outbound interfaces in VB classes. VC++ users do not have this limitation as the VC++
compiler supports custom outbound interfaces.
Acting as a client to a class with multiple outbound interfaces
IDL supports the implementation of multiple outbound interfaces on a single coclass, for example, a FeatureLayer
coclass has three outbound interfaces: ILayerEvents, IFeatureLayerSelectionEvents, and IObjectClassSchemaEvents.
In VC++ it is straightforward to QI to a second outbound interface on a coclass and act as a client sink to all the
method calls on those interfaces. VB, however, only provides native support for the default outbound interface on each
coclass by declaring a variable WithEvents.
To act as a sink to a nondefault outbound interface, a dummy helper coclass is inserted into the type library. This
helper coclass implements the outbound interface to which a VB developer requires access. In the client VB code, the
helper coclass is declared WithEvents and is then linked to the default outbound interface variable, providing access to
other outbound interfaces.
If you intend to implement more than one outbound interface on a coclass and are working in VC++, you could use
this technique if you want to allow VB developers access to those interfaces.
Creating a helper coclass using IDL
The helper coclass can be created entirely in IDL and, therefore, exists only in the type library of a component and the
registry, without having any implementation code at all. By convention, the helper coclass is named after the
outbound interface, omitting the initial 'I' and adding 'Helper' to the end of the class name.
Add code similar to that shown below to your IDL. Specify a new GUID and class name, change the helpcontext and
helpstring values if required (or omit the attributes entirely), and select the outbound (source) interface you want to
expose to VB developers.
[
uuid(ENTERXXX-GUID-HERE-1234-123456789ABC),
helpcontext(20053),
helpstring("Helper coclass for VB developers to access
nondefault outbound interface")
]
coclass MyEventInterfaceHelper
{
[default] interface IUnknown;
[default, source] interface IMyEventsInterface;
};
Also, ensure that your original coclass implements the outbound interface:
coclass MultipleOutboundInterfacesClass
{
[default] interface IUnknown;
...
[default, source] interface IDefaultOutboundInterface;
[source] interface IMyEventsInterface;
When your component is compiled, the VB client can access the nondefault outbound interfaces by declaring the class
level variable:
[Visual Basic 6.0]

Private m_myEvents As MyEventInterfaceHelper


The variable should be set by linking to an instance of the original coclass with the nondefault outbound interface.
[Visual Basic 6.0]

Dim myEventsCoclass As MultipleOutboundInterfacesClass


Set myEventsCoclass = New MultipleOutboundInterfacesClass
Set m_myEvents = myEventsCoclass

327

Now the events defined on IMyEventsInterface can be accessed in the usual manner from the m_myEvents variable.
Classes with multiple outbound interfaces
IDL allows a class to specify a default inbound and a default outbound interface.
VC++ developers can implement inbound and outbound interfaces as required.
VB developers, however, can only connect to the default outbound interface. For example, the FeatureLayer coclass
has the default outbound interface ILayerEvents.
[Visual Basic 6.0]

Private WithEvents m_pFeatLayer As esriCarto.FeatureLayer 'sinks ILayerEvents


The FeatureLayer coclass also implements the IFeatureLayerSelectionEvents interface. This is accessed by the helper
coclass FeatureLayerSelectionEvents, which has the default outbound interface IFeatureLayerSelectionEvents.
[Visual Basic 6.0]

Private WithEvents m_pFeatLyrSel As esriCarto.FeatureLayerSelectionEvents


...
Set m_pFeatLyrSel = m_pFeatLyr

IDL Standards
When the ArcObjects libraries were created, certain standards were applied to the IDL. Some of these standards may
be relevant if you are developing your own interfaces, particularly if you are using IDL to define the interface.
Internal or private items
Any coclasses or interfaces that you do not want to be accessible to other developers should be excluded from the
type library. In VC++ you can define a macro block to exclude certain items from the type library build process. In VB,
you would generally use the appropriate Private, Public, or Friend definitions.
Complete CoClass definitions
Every coclass definition should list all the public interfaces that it implements. As a QI should also support all inherited
interfaces, these interfaces should also be listed explicitly. For example, if a coclass implements IPersistStream, it is
recommended that it also list IPersist as IPersistStream inherits from IPersist.
You may want to also list the internal (private) interfaces for clarity and internal documentation purposes, but
comment them out so that they are not included in the type library.
Instance interfaces
Some coclasses implement interfaces only on certain instances of the class; these are referred to as instance
interfaces.
For example, the RasterLayer coclass has a number of instance interfaces; ITable, IAttributeTable, IDisplayTable,
ITableFields, and ITableSelection being just some of them. Note that for a specific instance, you should always be able
to QI for the same interfacesfor example, instance interfaces should not change during the lifetime of a specific
object. (Instance interfaces cannot be defined in VB, VC++ developers must add the QI implementation which may
not be a straightforward task.)
It is recommended that instead of using instance interfaces, you should consider creating subclasses that implement
these additional interfaces.
Noncreatable classes
All public noncreatable classes should be added to the IDL as coclasses, and the noncreatable attribute applied to the
class. This ensures that your noncreatable class will be publicly available and declared in the type libraryessential if
you want to link your component to a help system.
[
uuid(60B2E971-88D0-11D4-A697-00508B4A4114),
helpstring("Foo class"),
noncreatable
]
coclass Foo {
[default] interface IUnknown;
interface IFoo
interface IBar;
};
Member attributes
The use of pointer attributes, such as [unique], [ptr], and [ref], or field attributes, such as [size_is], [length_is],
[iid_is], and [switch_is], is not compatible with type library creation. You should avoid defining members that require
these attributes.
Version compatibility issues
The issue of binary compatibility applies to custom component development. To ensure binary compatibility at the IDL

328

level, check the following list of issues.

Datatypes or their members that were available at one version should not be removed at a later version.

There should be no syntactical or semantical changes to interfaces between versions.

Methods should not be newly restricted.

Coclasses should support the interfaces that they supported at a previous version.

Enum member values should not change between versions.

Help linking
Every coclass, interface, property, method, enumeration, and so on, should have a helpstring and a unique help
context ID. See the section on creating help systems in Chapter 2 for more details.

329

Geodatabase modeling
The geodatabase data model is an object-oriented data model for geographic data. To create blueprints of the objects,
their relationships, and their behavior, you can use UML, a graphical modeling language. Utilize the CASE tools to
create the storage medium (geodatabase schema) and object behavior (custom features and class extensions).
This appendix explores the concepts involved in modeling object behavior using UML and the Code Generation Wizard.
NOTE: CASE tool functionality does not apply to ArcView licenses.

Geodatabase modeling with UML


UML is the universal language of object modeling. With UML you can build object models that help you and others
better understand the system in development. The more complex a system is, the more difficulty you will have
understanding it. Modeling helps you understand such complex systems.
Using UML to model geodatabase structure
Using UML, you can create object models that include geodatabase elements. In the same way that the ArcGIS object
model diagrams help you understand ArcObjects, modeling geodatabase elements using UML lets you clearly visualize
the structure and behavior of your system. For example, you can easily see what feature classes are involved in a
geometric network, how features may be associated through relationship classes, or what services a custom feature
provides.
These elements may be subdivided into structural elements, parameterized elements, and custom behavior elements,
summarized in the table below.
Structural Elements

Parameterized Behavior

Custom Behavior

Feature datasets
Geometric networks
Feature classes
Relationship classes
Fields
Subtypes

Elements
Domains
Connectivity rules
Relationship rules

Custom features
Feature class extensions
Custom interfaces

This appendix provides a review of the modeling of custom behavior geodatabase elements using UML and CASE tools,
in particular the generation of custom COM classes. An overview of the general modelling process and a discussion of
the modeling concepts used are also given for context. You can find more information about modeling structural and
parameterized behavior elements in the ESRI book Building a Geodatabase.
Overview of using the ESRI CASE Tools
The ESRI CASE tools help you to create COM classes that implement the behavior of custom features and database
schemas in which the custom feature properties are maintained.
The basic procedure to use the CASE tools is summarized below.
First, create a UML object model of your geodatabase structure. The model should be
based on one of the templates provided by ESRI as part of ArcGIS. The templates are
available for Microsoft Visio or Rational Software Corporation's Rational Rose and can
be found in the CASE Tools subdirectory of your ArcGIS installation. The templates
contain a UML representation of the portion of the ArcGIS object model necessary to
model a geodatabase.
After you have created and checked your model, you need to export it to the
Microsoft Repository or to an XML Metadata Interchange (XMI) file. The intermediate
format you choose depends on your modeling software. XMI is a more recent
technology than the Repository.
At this point, you can use the ESRI Semantics Checker to verify the validity of the
model. This tool verifies that the geodatabase elements in a model are correctly
specified. The Semantics Checker is available from the template diagram in Visio,
and can also be run within Rational Rose using scripting; it can validate exported
data either in XMI format or in the Repository. For more information about the
semantics checker, see the ESRI book Building a Geodatabase.
You can then use the exported model in the ESRI CASE tools for code and schema
generation. These tools can use either XMI or Repository format.
You can generate code to implement custom behavior by using the ESRI Code
Generation Wizard add-in for Visual Studio. By running the wizard, you can create a
C++, ATL-based project with stub classes based on the objects defined in the UML
model. You can then add custom behavior to these classes and compile the project into a DLL. The DLL acts as a
carrier for the custom feature and class extension COM classes.
Finally you can create a geodatabase schema for your model using the ESRI Schema Wizard in ArcCatalog. This wizard
associates your custom features and class extensions with the feature classes created in the schema. Again, you can
find more information about creating schemas in Building a Geodatabase.
The ESRI Semantics Checker can be used to check the validity of an exported UML model.

330

The ESRI Code Generation Wizard can be used to produce stub code for custom geodatabase classes.
The ESRI Schema Wizard, a command available in ArcCatalog, can be used to create a geodatabase
schema based on a UML object model.

Creating UML object models for custom classes


The ESRI Template model
You can create new UML object models from the ESRI template diagrams. The diagrams contain information about the
geodatabase data access objects, specifically classes and interfaces relevant to the creation of custom features and
class extensions.
The template diagrams have a hierarchical structure based on UML
packages. A given model has, at the minimum, four packages. ESRI
Interfaces, ESRI Classes, Workspace, and Logical View (a logical root
which contains the other three packages). Interfaces of the geodatabase
API are defined under the ESRI interfaces package, for example
IRowEvents. Likewise, COM classes of the geodatabase API are defined
under ESRI Classes, for example ClassExtension. The workspace package
represents a geodatabase. Under it, you can create common
geodatabase elements, such as domains, feature datasets, and tables.

The UML Navigator window in Visio is used to explore the UML classes in
the ESRI template. The template contains classes representing
geodatabase data access objects. The UML Navigator also helps you
create your own UML model containing classes based on the ESRI
classes.

Modeling Concepts
To help understand the concepts involved in modeling custom behavior, look at the extract of an electric utilities UML
object model. The model represents a transformer custom feature (Transformer) and its associated class extension
(TransformerClassExtension).
Custom Features
Transformer is derived from the ESRI class SimpleJunctionFeature. This means a transformer will provide exactly the
same services as a simple junction feature. In other words, it will implement the same interfaces its parent
implements (type inheritance). In total, the transformer must implement approximately 20 system-defined interfaces,
such as IRow, IFeatureDraw, and ISimpleJunctionFeature. Clients of such interfaces include ArcMap, ArcCatalog, and
the geodatabase itself.
Custom features are modelled in UML by creating a class derived from one of the feature classes in
the ArcInfo UML model.
Custom features are COM classes that implement interfaces. This relationship is modeled in UML with a dependency
stereotyped as 'refines'. In the sample model, Transformer implements ITransformer, a developer-defined interface.
An interface is modeled as a UML class marked with the stereotype 'interface'. Interfaces are abstract classes because
they do not have code implementing them. In a way, they are a specification of the services the implementing class
must provide. Through these interfaces, custom features provide services on a specific domain, in this case, electrical
utilities. Applications developed on top of ArcGIS are the clients of these services.
Class extensions
Class extensions are created by type-inheriting either from ObjectClassExtension or FeatureClassExtension. In UML,
they are required to follow a naming conventionthe name of the class followed by "ClassExtension"
(TransformerClassExtension, for example).
Class extensions do not have fields but may implement developer-defined or optional ESRI geodatabase interfaces
such as IObjectClassValidation. Optional class extension interfaces are available under the ESRI interfaces package in
the UML templates.

331

You will find all the optional class extension interfaces under the ESRI Interfaces package in the UML
Navigator.
Schema creation with custom features and class extensions
When you use the ESRI Schema Creation Wizard, a feature in the UML object model will create a feature class in the
target geodatabase. For example, when the schema is created for the electric utilities model, Transformer will become
a feature class and its attributes will become fields (a Field named MainPeriodicity will contain integer values). Notice
the types of the fields are taken from the esriFieldType enumeration, while the types in the interfaces are C++
automation types.
During schema creation, if custom code was generated, you have the opportunity to assign the custom feature and its
class extension to the newly created feature class. For example, the Transformer class can be selected as the Behavior
class for the new feature class in the Behavior tab of the Properties dialog box for the feature class. The class
extension can also be specified here.

In the ESRI Schema Wizard you can specify that a feature class contains custom features and also
associate any class extensions.
The lists of available custom features and class extensions shown in the wizard are filled based on those registered in
the system; therefore, the DLL should be registered before running the wizard.
The generation of code to create custom features and class extensions is an optional step when using CASE tools. If
custom classes are not required by the model, the Schema Wizard will, by default, associate the appropriate ESRI COM
class with each created feature class.
Note that the Schema Wizard creates an instance of every custom feature or class extension registered in the system
and queries them for some information, for example, their feature type. To avoid crashes, custom features and class
extensions should handle error conditions properly during construction.

Generating code
ESRI Code Generation Wizard
The ESRI Code Generation Wizard works inside Visual Studio and can be used to generate an ATL-based C++ project

332

with stub code for the custom features and class extensions in your UML model.
To load the Code Generation Wizard, follow these steps:
1.

In Visual Studio, click Tools and click Customize.

2.

Click the Add-ins and Macro Files tab.

3.

Click Browse to search for the add-in. Click the Files of type dropdown list and choose Add-ins (.dll). Browse to
your ArcGIS installation directory, find the Bin subdirectory, and choose CodeGenWiz.dll. Click Open to add the
add-in to the list.

4.

Choose the ESRI Code Generation Wizard in the add-ins list, then close the Customize dialog box. The wizard is
now available on a toolbar in Visual Studio.

The ESRI Code Generation Wizard can be run from inside Visual Studio.
Using the code wizard
Close any open workspaces in Visual Studio, then run the ESRI Code Generation Wizard. The wizard will first ask you
to select the repository where your model is stored.
The wizard will display the hierarchy of objects in your model. At this point, you can define implementation reuse
options for each object in your model. For example, to generate a custom feature class for Transformer, ensure the
check box next to Transformer is selected.
It is not necessary to generate code for all the UML classes in a model. The model shown includes a class called Cable,
a generalization of SimpleEdgeFeature. In this case, Cable can be adequately represented by a SimpleEdgeFeature, as
it does not implement any custom interfaces or need to override any implementation of the existing
SimpleEdgeFeature. Therefore, you would not select this class for code generation in the wizard.

It is not necessary to generate code for all the UML classes in a model. In this case, Cable is not
selected for code generation, but Transformer is.
Code reuse by aggregation or containment
A custom feature is required to implement a number of system-defined interfaces so that ArcGIS can use it
successfully. Implementing all the interfaces locally could prove to be a difficult task. COM aggregation and
containment are simple techniques you can employ to reuse the implementation already present in ArcGIS COM
classes.

Aggregation and containment are techniques you can use to make use of the implementation already
present in ArcGIS geodatabase classes.
In both cases, the object to reuse is placed inside the object reusing the implementation. Each interface implemented

333

by the inner object can be directly exposed (COM aggregation) or indirectly exposed (COM containment). See the
discussion on COM aggregation and containment in Introduction to COM.
When developing custom features, COM containment should be used when the custom feature changes or adds
behavior to the implementation provided by the inner object. For example, in the electric utilities example, it is
decided to contain IRowEvents in Transformer so that a transformer may respond to the events of that interface.
However, a custom feature may if required aggregate all the interfaces implemented by its inner object and only
provide custom behavior through developer-defined interfaces (ITransformer in this example).

For each custom feature, the Code Generation Wizard will allow you to select what interfaces should
be contained or aggregated.
Class Descriptions
In the Code Generation Wizard you can optionally choose to create a class description COM class for each custom
feature in the model. Such COM classes describe the custom feature itself, so a feature class can be created using
ArcCatalog without using the Schema Wizard.
The class description class will implement the IObjectClassDescription and IFeatureClassDescription interfaces. Code
for class descriptions cannot be generated if the UML model includes relationship classes, subtypes, or geometric
networks; therefore, class descriptions cannot be generated for the electric utilities example.

Generated Code
The last screen of the ESRI Code Generation Wizard will prompt you to specify a new output workspace for the
generated code. After choosing the output, the wizard will create a Visual Studio C++ workspace containing the
following:
1.

Registration script (.rgs), header (.h), and implementation (.cpp) files for each custom feature and class
extension selected in the wizard

2.

IDL with the definition of COM classes, interfaces, and type library

3.

Other standard C++/ATL files

334

A view of the classes created in the DLL by the electric utilities example model.
Rgs and IDL files
The registration script creates the registry keys and values in the registry for each custom feature and class extension.
It also registers them under the appropriate component category.
The project's IDL contains the definition of the COM classes and interfaces created by the developer in the model.
ArcGIS software's COM classes and interfaces are imported using the importlib directive, so types, such as
IRowEvents, are available to the type library being created.
Header and implementation files
Attributes in interfaces yield accessor and mutator methods. For example, the Weight attribute in the ITransformer
interface generates the following IDL code:
[Visual C++]

[
[

propget ... ] HRESULT Weight([out, retval] double* pWeight);


propput ... ] HRESULT Weight([in] double Weight);

UML operations yield methods in the interface. The method NextMaintenance generates the following IDL code:
[Visual C++]

HRESULT NextMaintenance([out, retval] DATE* pNextMaintenance);


Read-only and write-only properties are created using methods prefixed with get_, put_, and propput_, as shown in
the following table.
Prefix / Sample

IDL

get_Foo : double

[ propget ] HRESULT Foo ([out, retval] double * pFoo);

put_Foo (Y : double)

[ propput ] HRESULT Foo ([in] double Y);

putref_Foo (Y : IY)

[ propputref ] HRESULT Foo ([in] IY * pIY);

Each time a custom feature is created, an instance of the inner ArcGIS COM class needs to be created as well. To
achieve this, the code wizard also adds stub code to the FinalConstruct of custom features (ATL calls FinalConstruct as
soon as the C++ class has been instantiated).
In the electric utility example, the C++ code generated for Transformer includes the creation of the inner
SimpleJunctionFeature in its FinalConstruct.
[Visual C++]

HRESULT Transformer::FinalConstruct()
{
// Creates instance of inner object
IUnknown * pOuter = GetControllingUnknown();
if (FAILED (CoCreateInstance(__uuidof(SimpleJunctionFeature),
pOuter,
CLSCTX_INPROC_SERVER,
IID_IUnknown,
(void**) &m_pInnerUnk)))
return E_FAIL;
In the same function, a QI is made for each COM-contained interface. A member variable will hold a reference to the
interface implemented by the inner object. For the Transformer, this affects the IRowEvents interface.
[Visual C++]

// QI for IRowEvents
if (FAILED(m_pInnerUnk->QueryInterface(IID_IRowEvents,
(void**)&m_pIRowEvents)))
return E_FAIL;
pOuter->Release();
return S_OK;

335

}
The header generated for the transformer declares the ATL COM MAP. These macros are used to specify which
interfaces are implemented locally and which are aggregated.
In the example, ITransformer and IRowEvents are implemented locally, and all other interfaces implemented by the
inner object are aggregated.
BEGIN_COM_MAP(Transformer)
COM_INTERFACE_ENTRY(ITransformer)
COM_INTERFACE_ENTRY(IRowEvents)
COM_INTERFACE_ENTRY_AGGREGATE_BLIND(m_pInnerUnk)
END_COM_MAP()
Stub code is also generated for the interfaces defined in the model, which by default returns E_NOTIMPL for each
method. It is your responsibility to add implementation code to these methods. In the code generated from electric
utilities model, the ITransformer interface in the transformer C++ class looks like the code below.
[Visual C++]

STDMETHODIMP Transformer::get_Weight(double* pWeight)


{
return E_NOTIMPL;
}
STDMETHODIMP Transformer::put_Weight(double Weight)
{
return E_NOTIMPL;
}
STDMETHODIMP Transformer::NextMaintenance(DATE* pNextMaintenance)
{
return E_NOTIMPL;
}
When coding the custom feature, you may add or change the implementation of a contained interface provided by the
inner object. For each method in the interface, you can choose to forward the call to the inner feature or use your own
implementation. The former option is used by the wizard by default.
For the electric utilities example, code is generated for the IRowEvents interface inside the transformer C++ class,
allowing you to write your own implementation for each method in the interface (recall that pointers to contained
interfaces are acquired in the FinalConstruct).
[Visual C++]

STDMETHODIMP Transformer::OnChanged()
{
return m_pIRowEvents->OnChanged();
}
STDMETHODIMP Transformer::OnDelete()
{
return m_pIRowEvents->OnDelete();
}...
See also TreeFeature Custom Feature Example.

336

S-ar putea să vă placă și