You are on page 1of 162

About Interoperating with COM

Code running under the control of the .NET Framework is called managed code; conversely, code
executing outside the .NET Framework is called unmanaged code. Component Object Model (COM)
is one example of unmanaged code. The .NET Framework interacts with COM via a technology
known as COM Interop.

For COM Interop to work, the Common Language Runtime (CLR) requires metadata for all the COM
types. This means that the COM type definitions normally stored in the type libraries need to be
converted to .NET metadata. This is easily accomplished with the Type Library Importer utility
(tlbimp.exe) that ships with the .NET Framework Software Developer Kit (SDK). This utility
generates interop assemblies containing the metadata for all the COM definitions in a type library.
Once metadata is available, .NET clients can seamlessly create instances of COM types and call its
methods as though they were native .NET instances.

Primary interop assemblies


Primary interop assemblies (PIAs) are the official, vendor-supplied .NET type definitions for
interoperating with underlying COM types. PIAs are strongly named by the COM library publisher to
guarantee uniqueness.

ESRI provides primary interop assemblies for all the ArcObjects type libraries that are implemented
with COM. ArcGIS .NET developers should only use the PIAs that are installed in the Global
Assembly Cache (GAC) during install if version 1.1 of the .NET Framework is detected. ESRI only
supports the interop assemblies that ship with ArcGIS. You can identify a valid ESRI assembly by its
public key (8FC3CC631E44AD86).

The ArcGIS installation program also installs the Microsoft Stdole.dll PIA, providing interop for
Object Linking and Embedding (OLE) font and picture classes, which are used by some ESRI
libraries. For more information about this Microsoft PIA, see the Microsoft Developer Network
Knowledge Base article number 328912.

COM wrappers
The .NET runtime provides wrapper classes to make both managed and unmanaged clients believe
they are communicating with objects within their respective environment. When managed clients
call a method on a COM object, the runtime creates a runtime-callable wrapper (RCW) that handles
the marshaling between the two environments. Similarly, the .NET runtime creates COM-callable
wrappers (CCWs) for the reverse case, in which COM clients communicate with .NET components.
The following illustration outlines this process:

Exposing .NET objects to COM


When creating .NET components for COM clients to use, observe the following guidelines to ensure
interoperability:

• Avoid using parameterized constructors.

• Avoid using static methods.

• Define event source interfaces in managed code.

• Include HRESULTs in user-defined exceptions.

• Supply globally unique identifiers (GUIDs) for types that require them.

• Expect inheritance differences.


For more information, see Registering .NET components with COM and Registering .NET
components in COM component categories and also review Interoperating with Unmanaged Code in
the Microsoft Developer Network (MSDN) help system.

Performance considerations
COM Interop adds a new layer of overhead to applications, but the overall cost of interoperating
between COM and .NET is small and often unnoticeable. However, the cost of creating wrappers and
having them marshal between environments does add up. If you suspect COM Interop is the
bottleneck in your application's performance, try creating a COM worker class that wraps all the
chatty COM calls into one function that managed code can invoke. This improves performance by
limiting the marshaling between the two environments. For more information, see Performance of
ArcObjects in .NET.

COM to .NET type conversion


Generally speaking, the type library importer imports types with the name they originally had in
COM. All imported types are also added to a namespace that has the following naming convention:
ESRI.ArcGIS.<name of the library>. For example, the namespace for the Geometry library is
ESRI.ArcGIS.Geometry. All types are identified by their complete namespace and type name.

Classes, interfaces, and members


All COM coclasses are converted to managed classes; the managed classes have the same name as
the original with "Class" appended. For example, the runtime-callable wrapper for the Point coclass
is called PointClass.

All classes also have an interface with the same name as the coclass that corresponds to the default
interface for the coclass. For example, the PointClass has a Point interface. The type library importer
adds this interface so clients can register as event sinks. See the following Class interfaces section
for more information.

The .NET classes also have class members that .NET supports, but COM does not. Each member of
each interface the class implements is added as a class member. Any property or method a class
implements can be accessed directly from the class rather than having to cast to a specific
interface. Since interface member names are not unique, name conflicts are resolved by adding the
interface name and an underscore as a prefix to the name of each conflicting member. When
member names conflict, the first interface listed with the coclass remains unchanged. See the
following code example:

[C#]

ESRI.ArcGIS.Geometry.PointClass thePt = new


ESRI.ArcGIS.Geometry.PointClass();

thePt.PutCoords(10.0F, 8.0F);

ESRI.ArcGIS.Geometry.IGeometry geom = thePt.Buffer(2.0F);

MessageBox.Show(geom.Dimension.ToString());

[VB.NET]

Dim thePoint As New PointClass

thePoint.PutCoords(10.0, 11.9)

Dim geom As IGeometry = thePoint.Buffer(2.0F)

MessageBox.Show(geom.Dimension.ToString())

Since interface member names are not unique, name conflicts are resolved by prefixing the
interface name and an underscore to the name of each conflicting member. When member names
conflict, the first interface listed with the coclass remains unchanged. For example, the MapClass
has members called AreaOfInterest and IBasicMap_AreaOfInterest.

Properties in C# that have by-reference or multiple parameters are not supported with the regular
property syntax. In these cases, it is necessary to use the accessory methods instead. See the
following code example:

[C#]

ILayer layer = mapControl.get_Layer(0);

MessageBox.Show(layer.Name);

Events
The type library importer creates several types that enable managed applications to sink to events
fired by COM classes. The first type is a delegate named after the event interface plus an
underscore followed by the event name, then the word EventHandler. For example, the
SelectionChanged event defined on the IActiveViewEvents interface has the following delegate
defined: IActiveViewEvents_SelectionChangedEventHandler. The importer also creates an event
interface with an "_Event" suffix added to the original interface name. For example,
IActiveViewEvents generates IActiveViewEvents_Event. Use the event interfaces to set up event
sinks.

Non-OLE automation-compliant types


COM types that are not OLE automation compliant generally do not work in .NET. ArcGIS contains a
few noncompliant methods, and these cannot be used in .NET. However, in most cases,
supplemental interfaces have been added that have the offending members rewritten compliantly.
For example, when defining an envelope via a point array, you can’t use
IEnvelope.DefineFromPoints; instead, you must use IEnvelopeGEN.DefineFromPoints. See the
following code example:

[VB.NET]

Dim pointArray(1) As IPoint

pointArray(0) = New PointClass

pointArray(1) = New PointClass

pointArray(0).PutCoords(0, 0)

pointArray(1).PutCoords(100, 100)

Dim env As IEnvelope

Dim envGEN As IEnvelopeGEN

env = New EnvelopeClass

envGEN = New EnvelopeClass

'Won't compile.

env.DefineFromPoints(2, pointArray)

'Doesn't work.

env.DefineFromPoints(2, pointArray(0))

'Works.
envGEN.DefineFromPoints(pointArray)

[C#]

IPoint[] pointArray = new IPoint[2];

pointArray[0] = new PointClass();

pointArray[1] = new PointClass();

pointArray[0].PutCoords(0, 0);

pointArray[1].PutCoords(100, 100);

IEnvelope env = new EnvelopeClass();

IEnvelopeGEN envGEN = new EnvelopeClass();

//Won't compile.

env.DefineFromPoints(3, ref pointArray);

//Doesn't work.

env.DefineFromPoints(3, ref pointArray[0]);

//Works.

envGEN.DefineFromPoints(ref pointArray);

Class interfaces
Class interfaces are created to help Visual Basic programmers transition to .NET; they are also
commonly used in code produced by the Visual Basic .NET Upgrade wizard or the code snippet
converter in Visual Studio .NET.

However, it is recommended that you avoid using the class interfaces in the ESRI interop
assemblies, as they may change in future versions of ArcGIS. This section explains a little more
about class interfaces.

In Visual Basic 6, the details of default interfaces were hidden from the user, and a programmer
could instantiate a variable and access the members of its default interface without performing a
specific query interface (QI) for that interface. For example, the following Visual Basic 6 code
instantiates the StdFont class and sets a variable equal to the default interface (IStdFont) of that
class:

[Visual Basic 6.0]


Dim fnt As New Stdole.StdFont

However, .NET does not provide this same ability. To allow Visual Basic developers a more seamless
introduction to .NET, the type library importer in .NET adds "class interfaces" to each interop
assembly, allowing COM objects to be used with this same syntax inside .NET. When an object
library is imported, a class interface RCW is created for each COM class; the name of the class
interface is the same as the COM class, for example, Envelope. All the members of the default
interface of the COM class are added to this class interface. If the COM class has a source interface
(is the source of events), then the class interface will also include all the events of this interface,
which helps a programmer to link up events.

A second RCW is created that represents the underlying COM class; the name of this is the same as
the COM class with a suffix of "Class," for example, EnvelopeClass. The class interface is linked to
the class by an attribute that indicates the class to which it belongs. This attribute is recognized by
the .NET compilers, which allow a programmer to instantiate a class by using its class interface.
The exception is classes that have a default interface of IUnknown or IDispatch, which are never
exposed on RCW classes because the members are called internally by the .NET Framework
runtime. In this case, the next implemented interface is exposed on the class interface instead. As
most ArcObjects define IUnknown as their default interface, this affects most ArcObjects classes.
For example, the Point COM class in the esriGeometry object library lists the IPoint interface as its
first implemented interface. In .NET, this class is accessed by using the Point class interface, which
inherits the IPoint interface, and the PointClass class.

The following code example shows that by declaring a variable type as a Point class interface, that
variable can be used to access the IPoint.PutCoords method from this class interface:

[C#]

ESRI.ArcGIS.Geometry.Point thePt = new ESRI.ArcGIS.Geometry.Point();

thePt.PutCoords(10, 8);

[VB.NET]

Dim thePt As ESRI.ArcGIS.Geometry.Point = New ESRI.ArcGIS.Geometry.Point()

thePt.PutCoords(10, 8)

The inherited interface of a class interface is not guaranteed to remain the same between versions
of ArcGIS; therefore, it is recommended that you avoid using the previous syntax. You can view
these types in the Visual Basic .NET Object Browser as shown in the following screen shot. When
using Visual Basic .NET, PointClass is not shown by default but can be made visible by selecting the
Show Hidden Members option.

In the C# Object Browser, you can see more clearly the class interface Point, its inherited interface
IPoint, and the class PointClass as shown in the following screen shot:
About binary compatibility
Most ArcGIS developers are familiar with binary compatibility. A component has binary compatibility
with a previous version of the same component if clients compiled against one version will run
against the new version without the client needing to be recompiled. Each component is associated
with a number of globally unique identifiers (GUIDs), such as the class ID, interface ID, and type
library ID. Maintaining the same GUIDs between component versions indicates that the versions are
binary compatible.

In Visual C++, developers control the GUIDs in a component by specifying them in the project's
Interface Definition Language (IDL) file, helping to control binary compatibility. In Visual Basic 6,
the Binary Compatibility compiler flag ensures that components maintain the same GUID each time
they are compiled. However, when this flag is not set, a new GUID is generated for each class each
time the project is compiled. This has the adverse effect of having to reregister the components in
their appropriate component categories after each recompilation.

To maintain binary compatibility in .NET, you can use the GuidAttribute class to manually specify a
class ID for your class. By specifying a GUID, you control when that GUID changes. If you do not
specify a GUID, the type library exporter will automatically generate one when you first export your
components to the Component Object Model (COM). Although the exporter will generally keep using
the same GUIDs on subsequent exports, this behavior is not guaranteed. It is recommended that
you specify GUIDs for any class that you expose to the COM. This includes any class that is inside a
project that is registered for COM Interop.

The following code example shows a GUID attribute being applied to a class. You can use the ESRI
GuidGen add-in for Visual Studio .NET to apply this attribute to an existing class with a new GUID.

[C#]

[GuidAttribute("9ED54F84-A89D-4fcd-A854-44251E925F09")]

public class SampleClass

{
//

[VB.NET]

<GuidAttribute("9ED54F84-A89D-4fcd-A854-44251E925F09")> _

Public Class SampleClass

'

End Class

Alternatively, if you are working in Visual Basic .NET, you can use the ComClass attribute instead.
This attribute can be used to specify GUIDs for the class, the default interface, and the default
events interface. You can add a new ComClass to your project by right-clicking the project in the
Solution Explorer and selecting Add New Item, then choosing ComClass in the Add New Item dialog
box. This not only applies the attribute but also generates and adds new GUIDs for use in the
attribute. Alternatively, you can apply the attribute to an existing class, as shown in the following
code example. For more information on different ways you can generate new GUIDs, see
Interoperating with COM.

[VB.NET]

<ComClass("B676A49F-2672-42ea-A378-20C17D1F2AFF", _

"5A90404F-8D1A-4b77-8F3F-C347A97FA34C", _

"58C9B6E4-B4F3-48dc-862A-181E566C4A31")> _

Public Class SampleClass

'

End Class

SummaryThis topic describes the System.__ComObject type and some issues related to it
including why casting a variable sometimes fails when it appears it should succeed and
creating the AppRef class in .NET.

Types and runtime callable wrappers


In .NET, each class, interface, enumeration, and so on, is described by its type. The Type class,
which is part of the .NET Framework, holds information about the data and function members of a
data type. When you create a new Component Object Model (COM) object in .NET via interop, you
get a reference to your object that is wrapped in a strongly typed runtime callable wrapper (RCW).
An RCW can hold a reference to a COM object inside a .NET application.

In the following code example, a variable called sym is declared as the ISimpleMarkerSymbol
interface type and is set to a new SimpleMarkerSymbolClass. The type of the variable sym is
retrieved and written to the debug window. If you were to run this code, you would find that the
sym type is SimpleMarkerSymbolClass, as you might expect; the variable holds a reference to the
ISimpleMarkerSymbol interface of the SimpleMarkerSymbolClass RCW.

[C#]
ESRI.ArcGIS.Display.ISimpleMarkerSymbol sym = new

ESRI.ArcGIS.Display.SimpleMarkerSymbolClass();

Debug.WriteLine(sym.GetType().FullName);

[VB.NET]

Dim sym As ESRI.ArcGIS.Display.ISimpleMarkerSymbol = New


ESRI.ArcGIS.Display.SimpleMarkerSymbolClass

Debug.WriteLine(CType(sym, Object).GetType.FullName)

In a different coding situation, you could get a reference to an RCW from another property or
method. For example, in the similar code that follows, the Symbol property of a renderer
(ISimpleRenderer interface) is retrieved, where the renderer uses a single SimpleMarkerSymbol to
draw.

[C#]

ESRI.ArcGIS.Display.ISimpleMarkerSymbol sym = rend.Symbol as

ESRI.ArcGIS.Display.ISimpleMarkerSymbol;

Debug.WriteLine(sym.GetType().FullName);

[VB.NET]

Dim sym As ESRI.ArcGIS.Display.ISimpleMarkerSymbol = rend.Symbol

Debug.WriteLine(CType(sym, Object).GetType.FullName)

Although you might expect to get the same output as before, you will actually find that the reported
type of sym is System.__ComObject.

The System.__ComObject type


The difference between the two previous excerpts of code is that, in the first, you create the symbol
using the New (or new) keyword and the type SimpleMarkerSymbolClass. When the code is
compiled, the exact type of the variable is discovered by the compiler using Reflection, and
metadata about that type is stored in the compiled code. When the code runs, the runtime then has
all the information (the metadata) that describes the exact type of the variable.

However, in the second example, you set the sym variable from the Symbol property of the
ISimpleRenderer interface. When this code is compiled, the only metadata that the compiler can
find is that the Symbol property returns an ISymbol reference; the type of the actual class of object
cannot be discovered. Although you can perform a cast to get the ISimpleMarkerSymbol interface of
the sym variable (or any other interface that the symbol implements), the .NET runtime does not
have the metadata required at run time to discover exactly what the type of the variable is. In this
case, when you access the Symbol property, the .NET runtime wraps the COM object reference in a
generic RCW called System.__ComObject. This is a class internal to the .NET Framework that can
be used to hold a reference to any kind of COM object; its purpose is to act as the RCW for an
unknown type of COM object.

Casting
In the second example, even if you know the exact type of class to which you have a reference,
the .NET runtime still does not have the metadata required to cast the variable to a strongly typed
RCW. This is shown in the following code example, as attempting a cast to the
SimpleMarkerSymbolClass type would fail:

[C#]

// The following line would result in sym2 being null, as the cast would
fail.

ESRI.ArcGIS.Display.SimpleMarkerSymbolClass sym2 = sym as

ESRI.ArcGIS.Display.SimpleMarkerSymbolClass;

[VB.NET]

' The following line would result in a runtime error, as the implicit cast
would fail.

Dim sym2 As ESRI.ArcGIS.Display.SimpleMarkerSymbol = sym

However, because the System.__ComObject class is specifically designed to work with COM objects,
it can always perform a query interface (QI) to any COM interfaces that are implemented by an
object. Therefore, casting to specific interfaces (as long as they are implemented on the object) will
be successful. See the following code example:

[C#]

ESRI.ArcGIS.Display.ISimpleMarkerSymbol sym3 = sym as

ESRI.ArcGIS.Display.ISimpleMarkerSymbol;

[VB.NET]

Dim sym3 As ESRI.ArcGIS.Display.ISimpleMarkerSymbol = sym

Singletons and System.__ComObject


In the previous examples, a strongly typed RCW is created when you instantiate the COM object by
using the new keyword, whereas if the object is preexisting, the type of the RCW is the generic
System.__ComObject. Sometimes when you use the new keyword to instantiate a COM object, you
are actually getting a reference to an object that already exists; this happens when attempting to
instantiate a singleton class that has previously been instantiated.

The .NET Framework is unable to wrap in a strongly typed RCW an instance of an object that has
previously been wrapped in the generic System.__ComObject RCW. If your code has encountered
such a situation, you may receive an error such as "Unable to cast object of type
System.__ComObject to type <Typename>." See the following code example:

[C#]

ESRI.ArcGIS.Display.IStyleGallery sg = new
ESRI.ArcGIS.Framework.StyleGalleryClass();

[VB.NET]

Dim sg As ESRI.ArcGIS.Display.IStyleGallery = = New


ESRI.ArcGIS.Framework.StyleGalleryClass
This error can occur even though you have declared your variable using the interface name rather
than the class name, as in the previous example. The problem occurs because, when your code
instantiates an object, the .NET runtime first attempts to wrap the object in the strongly typed class
type (the type stated after the new keyword) before attempting a cast to the interface type. The
cast to the strongly typed RCW cannot succeed because the COM object has previously been
wrapped in the generic System.__ComObject wrapper. This can occur in situations beyond your
control. For example, other ArcObjects tools written in .NET from third parties may wrap an object
in the generic wrapper, causing your code to fail.

The solution is to use the Activator class (shown as follows) to safely wrap singleton objects in a
strongly typed RCW when you first get a reference to them. Additionally, you should generally
always declare variables holding RCWs using an interface rather than a class Type.

Using the Activator class to create singletons


If you use the CreateInstance method of the Activator class instead of the new keyword to
instantiate singletons, you can avoid such errors, as Activator is able to get the required metadata
to perform the cast. See the following code example:

[C#]

Type t = Type.GetTypeFromProgID("esriFramework.StyleGallery");

System.Object obj = Activator.CreateInstance(t);

IStyleGallery sg = obj as IStyleGallery;

[VB.NET]

Dim t As Type = Type.GetTypeFromProgID("esriFramework.StyleGallery")

Dim obj As System.Object = Activator.CreateInstance(t)

Dim pApp As ESRI.ArcGIS.Display.IStyleGallery = obj

You can use this technique to instantiate the AppRef class; however, the AppRef class can only be
created within an ArcGIS application. (The type is the generic System.__ComObject RCW.) See the
following code example:

[C#]

Type t = Type.GetTypeFromProgID("esriFramework.AppRef");

System.Object obj = Activator.CreateInstance(t);

ESRI.ArcGIS.Framework.IApplication pApp = obj as


ESRI.ArcGIS.Framework.IApplication;

[VB.NET]

Dim t As Type = Type.GetTypeFromProgID("esriFramework.AppRef")

Dim obj As System.Object = Activator.CreateInstance(t)

Dim pApp As ESRI.ArcGIS.Framework.IApplication = obj

For more information about RCWs and interop, refer to the book by Adam Nathan, .NET and COM–
The Complete Interoperability Guide, Sams Publishing, 2002.
SummaryThis topic provides information on the interoperation of Component Object Model (COM)
and .NET including how memory is managed in the two different models.

AOUninitialize.Shutdown
Unexpected errors can occur when a stand-alone application attempts to shut down. For example,
you may have experienced errors on exit from an ArcGIS Engine application hosting a MapControl
with a loaded map document, with an error message similar to The instruction x references memory
at x. The memory could not be read. Such an error will be outside the scope of any error handling
statements in your code.

These errors can often result when COM objects remain in memory longer than expected,
preventing the correct unloading of the COM libraries from the process when it shuts down. To help
stop such errors, a static Shutdown function has been added to the ESRI.ArcGIS.ADF assembly. This
function can help avoid such errors by ensuring that COM references no longer in use are unloaded
prior to the process shutdown.

The following code example shows how you can use this function in the Disposed method of a form:

[VB.NET]

Private Sub Form1_Disposed(ByVal sender As Object, ByVal e As


System.EventArgs) Handles MyBase.Disposed

ESRI.ArcGIS.ADF.COMSupport.AOUninitialize.Shutdown()

End Sub

[C#]

private void Form1_Disposed(object sender, System.EventArgs e)

ESRI.ArcGIS.ADF.COMSupport.AOUninitialize.Shutdown();

This function can only help with unloading libraries for which no COM objects still exist; it is most
useful to apply this function after any COM objects have been disposed of. For example, in an
ArcGIS Engine windows application with a startup form, it is useful to place your call to
AOUninitialize.Shutdown in the Form Disposed event handler.

ComReleaser class
The AOUninitialize.Shutdown function handles many of the shutdown problems in stand-alone
applications, particularly relating to applications containing the Engine Controls. However, you may
still experience problems where COM objects remain in memory in your application and need to be
explicitly released from memory.

To ensure a COM object is released when it goes out of scope, you can use the ComReleaser class.
The ComReleaser class can be found in the ESRI.ArcGIS.ADF.dll assembly. You may want to use
this class to keep track of your COM objects, as it can help ensure your object references are
disposed of when your code terminates. See the ESRI.ArcGIS.ADF library reference documentation
for more information on using this class.

Internally, this class uses the ReleaseCOMObject method on the


System.Runtime.InteropServices.Marshal class to ensure COM object references are terminated. It
is important to note that this should only be called on objects that are no longer required in any
managed code in the process. See the following Marshal.ReleaseComObject section and the
Microsoft Developer Network (MSDN) documentation on ReleaseComObject for more information.
Marshal.ReleaseComObject
In .NET code, references to COM objects are held via runtime callable wrappers (RCWs), managed
objects that act as proxy objects for the underlying COM object. The .NET garbage collector is
responsible for clearing managed objects from memory, which happens in a nondeterministic way.
In a case where a COM object holds system resources (file handles, database connections, and so
on), you may need to explicitly release certain COM objects to free the resources held by the COM
object.

Such problems can cause different types of errors depending on the circumstances. For example,
repeatedly opening geodatabase cursors against a personal geodatabase without ensuring the last
cursor was freed may cause an error indicating that no more tables can be opened. In other
situations, error messages may occur on application shutdown, as object references remain in
memory; the StyleGallery is one such class that can often cause errors on exit if you do not
explicitly release it.

To fully free the COM object underlying an RCW from memory at a deterministic point, it is possible
to use the ReleaseComObject method on the Marshal class, which is part of the
System.Runtime.InteropServices namespace in the .NET Framework. Calling ReleaseComObject will
decrease the reference count held on an RCW; once the reference count on the RCW reaches zero
(which may require repeated calls to ReleaseComObject), the RCW is marked for garbage collection.
If no other COM objects hold a reference to the underlying COM object at that point, the COM
runtime will also clear up the COM object itself.

Calling ReleaseComObject affects all managed references to a COM object in the current process.
You should be particularly careful when calling this method from an in-process component, such as
a dynamic-link library (DLL), which is loaded into an ArcGIS Desktop application. It is not
recommended that you call ReleaseComObject on any object to which another managed component
may have a reference. For example, if you have stored a reference to MxDocument and call
ReleaseComObject on that reference, then any other managed in-process component is unable to
access the MxDocument object from that point on. If you are creating a stand-alone application and
have the advantage of controlling all the managed code in that application, you are able to
determine more precisely when you can release the COM objects.
The following example code shows how you can call ReleaseComObject to release a StyleGallery
object. The code shown recursively calls ReleaseComObject until the returned value is zero. This
indicates there are no longer any managed references to the StyleGallery, and such code should
only be used when you are sure no other managed code will require further access to the object.

[VB.NET]

Sub Main()

Dim styCls As ESRI.ArcGIS.Display.IStyleGallery = New


ESRI.ArcGIS.Framework.StyleGalleryClass

' Use the StyleGalleryClass here.

Dim refsLeft As Integer = 0

Do

refsLeft =
System.Runtime.InteropServices.Marshal.ReleaseComObject(styCls)

Loop While (refsLeft > 0)

End Sub

[C#]
private void MyFunction()

ESRI.ArcGIS.Display.IStyleGallery styCls = new

ESRI.ArcGIS.Framework.StyleGalleryClass()as

ESRI.ArcGIS.Display.IStyleGallery;

// Use the StyleGalleryClass here.

int refsLeft = 0;

do

refsLeft = Marshal.ReleaseComObject(styCls);

while (refsLeft > 0);

You may also want to call ReleaseComObject on objects that you create in a loop (such as
enumerators), because you can be sure the objects are freed in a timely manner, rather than
waiting for the garbage collection process to perform the cleanup. This is useful when dealing with
cursor objects and other geodatabase objects that hold resources as well as style gallery
enumerators and item objects. See the following Releasing geodatabase cursors section for more
information.

For more information about the garbage collection process and the ReleaseComObject method, refer
to MSDN documentation.

Releasing geodatabase cursors


Some objects may lock or use resources that the object frees only in its destructor. For example, in
the ESRI libraries, a geodatabase cursor can acquire a shared schema lock on a file-based feature
class or table on which it is based or can hold on to a Spatial Database Engine (SDE) stream.

While the shared schema lock is in place, other applications can continue to query or update the
rows in the table, but they cannot delete the feature class or modify its schema. In the case of file-
based data sources, such as shapefiles, update cursors acquire an exclusive write lock on the file,
which prevents other applications from accessing the file for read or write purposes. The effect of
these locks is that the data may be unavailable to other applications until all the references on the
cursor object are released.

In the case of SDE data sources, the cursor holds on to an SDE stream, and if the application has
multiple clients, each may get and hold on to an SDE stream, eventually exhausting the maximum
allowable streams. The effect of the number of SDE streams exceeding the maximum is that other
clients will fail to open their own cursors to query the database. In other cases, the server may
exhaust its available memory.

In .NET, your reference on the cursor (or any other COM object) will not be released until garbage
collection occurs; only then will the resources be released. Therefore, if you are performing a
number of operations using objects that lock database resources, you may find that these resources
can accumulate up to the maximum allowable limits for the data source. At this point, errors can
vary depending on that data source; they may indicate the lack of resources. For example,
accessing personal geodatabase tables may indicate only 255 connections to tables are allowed. In
other cases, the exceptions may not be obviously related to the lack of resources.

Calling GC.Collect, the .NET method to initiate garbage collection at a known point, can be used to
clear any pending objects. However, forcing garbage collection is not generally recommended, as
the call itself can be slow and forcing the collection can also interfere with optimum garbage
collector operation. The correct approach is to free objects that may hold resources by using
Marshal.ReleaseComObject. Typically, you should always release cursor objects in this way (for
example, objects implementing IFeatureCursor). You may also want to release objects
implementing ISet as well as geodatabase enumerators. Again, you should not free any object that
needs to be accessed from elsewhere within .NET code; for example, objects that are long lived that
you did not create in your own code.

In a Web application or Web service servicing multiple concurrent sessions and requests, relying on
garbage collection to release references on objects holding resources will result in cursors and their
resources not being released in a timely manner. Similarly, in an ArcGIS Desktop or Engine
application, relying on garbage collection to release such references can result in your application or
other applications receiving errors.

ArcGIS Desktop and Engine


If you are using geodatabase cursors in your code, you may want to call Marshal.ReleaseComObject
on each cursor object when you no longer require it to ensure any geodatabase resources are
released in a timely manner. The following code example demonstrates this pattern:

[VB.NET]

For i As Integer = 1 To 2500

Dim qu As IQueryFilter = New QueryFilterClass

qu.WhereClause = "Area = " & i.ToString()

Dim featCursor As IFeatureCursor = featClass.Search(qu, True)

' Use the feature cursor as required.

System.Runtime.InteropServices.Marshal.ReleaseComObject(featCursor)

Next i

[C#]

for (int i = 1; i < 2500; i++)

IQueryFilter qu = New QueryFilterClass();

qu.WhereClause = @"Area = " + i.ToString();

IFeatureCursor featCursor = featClass.Search(qu, true);

// Use the feature cursor as required.

System.Runtime.InteropServices.Marshal.ReleaseComObject(featCursor);

}
ArcGIS Engine
The ComReleaser class plays an important role in working with singletons in ArcGIS Engine. As
mentioned previously, COM objects left in memory for an extended period after use may cause
problems and errors on shutdown. Singletons are a type of COM object that have only one instance
per process; you may find your code results in errors if singletons are left hanging (a process known
as pinning). In the case of singletons, it is necessary to always unpin (or release) the reference,
regardless of the specific API you are using. For non-.NET APIs, simply set the member variable
equal to nothing. For the .NET API, release the singleton COM object using the ComReleaser class
as specified in the previous ComReleaser Class section. For more information on specific singletons
used in ArcGIS Engine, see Using the control commands: Singleton objects.

ArcGIS Server
To ensure a COM object is released when it goes out of scope, the WebControls assembly contains a
helper object called WebObject. Use the ManageLifetime method to add your COM object to the set
of objects that will be explicitly released when the WebObject is disposed of. You must scope the
use of WebObject within a using block so that any object (including your cursor) that you have
added to the WebObject using the ManageLifetime method will be explicitly released at the end of
the using block.

The following code example demonstrates this pattern:

[VB.NET]

Private SubSystem.Object doSomething_Click(ByVal sender As System.Object,


ByVal e As System.EventArgs) Handles doSomething.Click

Dim webobj As WebObject = New WebObject

Dim ctx As IServerContext = Nothing

Try

Dim serverConn As ServerConnection = New ServerConnection("doug", True)

Dim som As IServerObjectManager = serverConn.ServerObjectManager

ctx = som.CreateServerContext("Yellowstone", "MapServer")

Dim mapsrv As IMapServer = ctx.ServerObject

Dim mapo As IMapServerObjects = mapsrv

Dim map As IMap = mapo.Map(mapsrv.DefaultMapName)

Dim flayer As IFeatureLayer = map.Layer(0)

Dim fClass As IFeatureClass = flayer.FeatureClass

Dim fcursor As IFeatureCursor = fClass.Search(Nothing, True)

webobj.ManageLifetime(fcursor)

Dim f As IFeature = fcursor.NextFeature()

Do Until f Is Nothing

' Do something with the feature.

f = fcursor.NextFeature()
Loop

Finally

ctx.ReleaseContext()

webobj.Dispose()

End Try

End Sub

[C#]

private void doSomthing_Click(object sender, System.EventArgs e)

using(WebObject webobj = new WebObject())

ServerConnection serverConn = new ServerConnection("doug", true);

IServerObjectManager som = serverConn.ServerObjectManager;

IServerContext ctx = som.CreateServerContext("Yellowstone",


"MapServer");

IMapServer mapsrv = ctx.ServerObject as IMapServer;

IMapServerObjects mapo = mapsrv as IMapServerObjects;

IMap map = mapo.get_Map(mapsrv.DefaultMapName);

IFeatureLayer flayer = map.get_Layer(0)as IFeatureLayer;

IFeatureClass fclass = flayer.FeatureClass;

IFeatureCursor fcursor = fclass.Search(null, true);

webobj.ManageLifetime(fcursor);

IFeature f = null;

while ((f = fcursor.NextFeature()) != null)

// Do something with the feature.

ctx.ReleaseContext();

}
The WebMap, WebGeocode, and WebPageLayout objects also have a ManageLifetime method. If you
are using, for example, a WebMap and scope your code in a using block, you can rely on these
objects to explicitly release objects you add with ManageLifetime at the end of the using block.

About shutting down ArcGIS .NET applications (ESRI.ArcGIS.ADF)


To help unload Component Object Model (COM) references in .NET applications, the AOUninitialize
class provides the static (shared in VB.NET) function Shutdown. This class is part of the
ESRI.ArcGIS.ADF.COMSupport namespace in the ESRI.ArcGIS.ADF.dll assembly. For more
information on shutting down ArcGIS .NET applications, see How to release COM references. See
the following code example:

[C#]

ESRI.ArcGIS.ADF.COMSupport.AOUninitialize.Shutdown();

[VB.NET]

ESRI.ArcGIS.ADF.COMSupport.AOUninitialize.Shutdown()

In this topic

• Using strings and embedded images directly (without localization)

• Creating resource files

• Resources in Visual Studio .NET

• Creating a .resx file for string resources

• Creating resource files for image resources

• Creating resource files using .NET SDK tools

• Creating resource files programmatically

• Compiling a .resx file into a .resources file

• Using resources with localization

• How to use resources with localization

• Developer sample with localization

• Embedding a default .resources file in a project

• Creating .resources.dll files for cultures supported by a project

Using strings and embedded images directly (without localization)


If your customization does not support localization now and won't in the future, you can use strings
and images directly without resource files. For example, strings can be specified and used directly in
your code. See the following code example:

[C#]

this.textBox1.Text = "My String";

[VB.NET]
Me.TextBox1.Text = "My String"

Image files, such as .bmp, .jpg, .png, and so forth, can be embedded in your assembly as follows:

1. Right-click the project in the Solution Explorer, click Add, then click Add Existing Item.
2. In the Add Existing Item dialog box, browse to your image file and click Open.
3. In the Solution Explorer, select the image file you just added, then press F4 to display its
properties.
4. Set the Build Action property to Embedded Resource. See the following screen shot:

Now you can reference the image in your code. For example, the following code example creates a
bitmap object from the first embedded resource in the assembly:

[C#]

string[] res = GetType().Assembly.GetManifestResourceNames();

if (res.GetLength(0) > 0)

System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(GetType()

.Assembly.GetManifestResourceStream(res[0]));

...

[VB.NET]

Dim res() As String = GetType(Form1).Assembly.GetManifestResourceNames()

If (res.GetLength(0) > 0)

Dim bmp As System.Drawing.Bitmap = New System.Drawing.Bitmap( _

GetType(Form1).Assembly.GetManifestR
esourceStream(res(0)))

...
Creating resource files
Before providing localized resources, you should be familiar with the process of creating resource
files for your .NET projects. Even if you don't intend to localize your resources, you can still use
resource files instead of images and strings directly as previously described.

Resources in Visual Studio .NET


Visual Studio .NET projects use an Extensible Markup Language (XML) based file format to contain
managed resources. These XML files have the extension .resx and can contain any kind of data
(images, cursors, and so forth) as long as the data is converted to the American Standard Code for
Information Interchange (ASCII) format. Resx files are compiled to .resources files, which are
binary representations of the resource data. Binary .resources files can be embedded by the
compiler into either the main project assembly or a separate satellite assembly that contains only
resources. Various options exist for how you can create resource files.

Creating a .resx file for string resources


If you're only localizing strings (not images or cursors), you can use Visual Studio .NET to create a
new .resx file that will be compiled automatically into a .resources module embedded in the main
assembly.

1. Right-click the project name in the Solution Explorer, click Add, then click Add New Item.
2. In the Add New Item dialog box, click Assembly Resource File in the Templates area. See
the following screen shot:

3. Open the new .resx file in Visual Studio and add name-value pairs for the culture-specific
strings in your application. See the following screen shot:
4. When you compile your project, the .resx file will be compiled to a .resources module inside
your main assembly.

Creating resource files for image resources


The process of adding images, icons, or cursors to a resource file in .NET is more complex than
creating a file containing only string values, because the tools currently available in the Visual
Studio .NET integrated development environment (IDE) can only be used to add string resources.

Creating resource files using .NET SDK tools


A number of sample projects are available with Visual Studio .NET that can help you with resource
files. A list of the available tools for working with resources can be found on the Microsoft Web site.
For more information, see Resource Tools.

The ResEditor .NET Framework Software Developer Kit (SDK) sample can be used to add images,
icons, image lists, and strings to a resource file. The tool cannot be used to add cursor resources.
Files can be saved as either .resx or .resources files. See the following screen shot:

The ResEditor .NET Framework SDK sample is provided by Microsoft as source code. You must build
the sample first if you want to create resource files using this tool. You can find information on
building the SDK samples under the SDK subdirectory of your Visual Studio .NET installation.
Creating resource files programmatically
You can create XML .resx files containing resources programmatically by using the
ResXResourceWriter class (part of the .NET framework). You can create binary .resources files
programmatically by using the ResourceWriter class (also part of the .NET framework). These
classes will allow more flexibility to add the kind of resources you require.

These classes can be useful if you want to add resources that cannot be handled by the .NET
framework SDK samples and tools, for example, cursors. The basic usage of the two classes is
similar; first, create a new resource writer class specifying the file name, then add resources
individually using the AddResource method.

The following code example demonstrates how you can create a new .resx file using the
ResXResourceWriter class, and add a bitmap and cursor to the file:

[C#]

System.Drawing.Image img = (System.Drawing.Bitmap)new


System.Drawing.Bitmap(

"ABitmap.bmp");

System.Windows.Forms.Cursor cur = new


System.Windows.Forms.Cursor("Pencil.cur");

System.Resources.ResXResourceWriter rsxw = new


System.Resources.ResXResourceWriter(

"en-GB.resx");

rsxw.AddResource("MyBmp_jpg", img);

rsxw.AddResource("Mycursor_cur", cur);

rsxw.Close();

[VB.NET]

Dim img As System.Drawing.Image = CType(New


System.Drawing.Bitmap("ABitmap.bmp"), System.Drawing.Image)

Dim cur As New System.Windows.Forms.Cursor("Pencil.cur")

Dim rsxw As New System.Resources.ResXResourceWriter("en-AU.resx")

rsxw.AddResource("MyBmp_jpg", img)

rsxw.AddResource("Mycursor_cur", cur)

rsxw.Close()

Compiling a .resx file into a .resources file


XML-based .resx files can be compiled to binary .resources files by using either the Visual Studio
IDE or the ResX Generator (ResXGen) sample in the tutorial.
• Any .resx file included in a Visual Studio project will be compiled to a .resources module
when the project is built. For more information, see Creating .resources.dll files for cultures
supported by a project on how multiple resource files are used for localization.

• You can convert a .resx file into a .resources file independently of the build process by
using the .NET framework SDK command resgen, as the following example shows: resgen
PanToolCS.resx PanToolCS.resources.

Using resources with localization


The following explains how you can localize resources for your customizations.

How to use resources with localization


In .NET, a combination of a specific language and country/region is called a culture. For example,
the American dialect of English is indicated by the string "en-US," and the Swiss dialect of French is
indicated by "fr-CH."

If you want your project to support various cultures (languages and dialects), you should construct
separate .resources files containing culture-specific strings and images for each culture.

When you build a .NET project that uses resources, .NET embeds the default .resources file in the
main assembly. Culture-specific .resources files are compiled into satellite assemblies (using the
naming convention <Main Assembly Name>.resources.dll) and placed in subdirectories of the main
build directory. The subdirectories are named after the culture of the satellite assembly they
contain. For example, Swiss-French resources would be contained in a fr-CH subdirectory.

When an application runs, it automatically uses the resources contained in the satellite assembly
with the appropriate culture. The appropriate culture is determined by the Windows settings. If a
satellite assembly for the appropriate culture cannot be found, the default resources (those
embedded in the main assembly) will be used instead.

Developer sample with localization


The Visual Basic .NET and C# examples of the Pan Tool developer sample illustrate how to localize
resources for German language environments. The sample can be found in the Developer
Samples\ArcMap\Commands and Tools\Pan Tool folder. Strictly speaking, the sample only requires
localized strings, but the images have been changed for the "de" culture as well, to serve as
illustration.

A batch file named buildResources.bat is provided in the Pan Tool sample to facilitate creating the
default .resources files and the culture-specific satellite assemblies.

Embedding a default .resources file in a project

1. Right-click the project name in the Solution Explorer, click Add, then click Add Existing Item
to navigate to your .resx or .resources file.
2. In the Solution Explorer, choose the file you just added and press F4 to display its
properties.
3. Set the Build Action property to Embedded Resource.

This will ensure that your application always has a set of resources to fall back on if there isn't a
resource .dll for the culture in which your application runs.

Creating .resources.dll files for cultures supported by a project

1. Ensure you have a default .resx or .resources file in your project.


2. Take the default .resx or .resources file and create a separate localized file for each culture
you want to support.
• Each file should contain resources with the same names; the Value of each
resource in the file should contain the localized value.

• Localized resource files should be named according to their culture, for example,
<BaseName>.<Culture>.resx or <BaseName>.<Culture>.resources.
3. Add the new resource files to the project, ensuring each one has its Build Action set to
Embedded Resource.
4. Build the project.

The compiler and linker will create a separate satellite assembly for each culture. The satellite
assemblies will be placed in subdirectories under the directory holding your main assembly. The
subdirectories will be named by culture, allowing the .NET runtime to locate the resources
appropriate to the culture in which the application runs.

The main (default) resources file will be embedded in the main assembly.

For more information on working with resources in .NET, see the Microsoft Developer Network
(MSDN) tutorial Resources and Localization using the .NET Framework SDK.

About performance of ArcObjects


ArcObjects is based on the Component Object Model (COM). For .NET objects to interoperate with
COM objects, an intermediate layer is needed. The .NET Framework contains the Interop application
programming interface (API), which acts as the intermediate layer between .NET and COM. The
Interop API provides Runtime Callable Wrappers (RCWs) as the mechanism that allows the use of
COM objects in .NET.

As shown in the following code example, define a COM object in .NET:

[C#]

ESRI.ArcGIS.Geometry.Point pt = new ESRI.ArcGIS.Geometry.Point();

[VB.NET]

Dim pt As ESRI.ArcGIS.Geometry.Point = New ESRI.ArcGIS.Geometry.Point()

An RCW object is defined instead of the COM object itself. When the object is instantiated (using the
new keyword), the RCW object creates the appropriate COM object. When you call the members of
the object in .NET, the parameters of the method call will be marshaled from the .NET process to
the COM process, and the return value will be marshaled back.

Example 1: Looping ArcObjects geometry operations


To analyze the performance of ArcObjects in .NET, you need to consider the overhead of creating
wrapper objects and marshaling, as compared to the actual running time of your ArcObjects code.
The following code example shows this comparison.
This example code is not intended as a benchmark of ArcObjects performance in .NET but
demonstrates possible differences in the speed of execution of ArcObjects code, which you
may want to consider when creating applications. The example code was run on a machine with a
processor speed of 3 GHz, with NET Framework version 2.0, using Visual Studio .NET 2005.
First, create a Windows application in C# or VB.NET, add a form with one button, and perform some
simple geometry operations in a loop when the button is clicked.

[C#]

private void button1_Click(object sender, System.EventArgs e)


{

// Prepare the objects.

ESRI.ArcGIS.Geometry.IPoint p1, p2;

p1 = new ESRI.ArcGIS.Geometry.Point();

p2 = new ESRI.ArcGIS.Geometry.Point();

p1.PutCoords(1, 2);

p2.PutCoords(3, 4);

ESRI.ArcGIS.Geometry.ILine pLine = new ESRI.ArcGIS.Geometry.Line();

// Time a loop of geometry operations.

int tick1, tick2, tick3;

tick1 = System.Environment.TickCount;

for (long i = 0; i <= 10000000; i++)

pLine.PutCoords(p1, p2);

tick2 = System.Environment.TickCount;

tick3 = tick2 - tick1;

System.Windows.Forms.MessageBox.Show(tick3.ToString());

[VB.NET]

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As


System.EventArgs) Handles Button1.Click

' Prepare the objects.

Dim p1, p2 As ESRI.ArcGIS.Geometry.IPoint

p1 = New ESRI.ArcGIS.Geometry.Point

p2 = New ESRI.ArcGIS.Geometry.Point

p1.PutCoords(1, 2)

p2.PutCoords(3, 4)

Dim pLine As ESRI.ArcGIS.Geometry.ILine = New ESRI.ArcGIS.Geometry.Line


' Time a loop of geometry operations.

Dim tick1, tick2, tick3 As Integer

tick1 = System.Environment.TickCount

Dim i As Integer

For i = 0 To 10000000

pLine.PutCoords(p1, p2)

Next i

tick2 = System.Environment.TickCount

tick3 = tick2 - tick1

System.Windows.Forms.MessageBox.Show(tick3.ToString())

End Sub

This code took an average of approximately 30 seconds to run in C# and VB.NET. For
comparison, use Visual C++ to create a standard executable (EXE), using the following
code example. (The GetTickCount Windows API call replaces the .NET Framework TickCount
property.)

[VC++]
LRESULT OnOK(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
IPointPtr ipP1(CLSID_Point);
IPointPtr ipP2(CLSID_Point);
ipP1->PutCoords(1.0,2.0);
ipP2->PutCoords(3.0,4.0);
ILinePtr ipLine(CLSID_Line);
DWORD tick1, tick2, tick3;
tick1 = ::GetTickCount();

for(long i=0L; i<10000000L; i++)


{
ipLine->PutCoords(ipP1, ipP2);
}
tick2 = ::GetTickCount();
tick3 = tick2 - tick1;

TCHAR msg[255];
_stprintf(msg, _T("Total Time: %ld ms"), tick3);
::MessageBox(0, msg, _T(""), MB_OK);

return 0;
}

This equivalent Visual C++ code takes approximately 5 seconds to execute, making the
code about six times faster than .NET. However, the previous example is an extreme
case, performing a small operation thousands of times that executes very quickly. In cases like this,
the time spent in the interoperation between COM and .NET is likely to dominate the total running
time.
Example 2: Creating and populating a geodatabase
Consider instead example code that creates and populates a personal geodatabase. A personal
geodatabase is first created, then populated with features by importing the shapefiles that comprise
the sample data. Subtypes are created, and a geometric network with connectivity rules is built. A
composite relationship class is also created. In contrast to Example 1, this code consists of
operations that take longer to execute.

In C# and VB.NET, this code takes an average of 25 seconds to run, and in VB6, the execution time
also averages approximately 25 seconds. How can the .NET code execute just as
quickly, considering the extra instructions required for the interop layer? The VB6 code is executed
by the Visual Basic runtime, which can be less efficient than the .NET Framework. For this example
then, no difference is seen in performance between the environments.

Example 1 revisited: Creating a proxy class in Managed C++


Realistically, you may need to perform the kind of operation that took so much longer in .NET—
large iterations of small operations. Do you have to take the performance hit?

Not necessarily. You have the choice to write a proxy class in Managed C++ where you can place
ArcObjects code that uses the interop layer heavily. Once you expose this code as public methods,
you can call the methods from your C# or VB.NET code. Since the interop layer is only required
when you call the methods of this proxy class, the execution time can be greatly reduced.

For example, to perform the previous operations, you can create a proxy class in Managed C++.
This class will contain a function that performs the loop of geometry operations.

[C++]

#using "C:\Program Files\ArcGIS\Dotnet\ESRI.ArcGIS.System.dll"


#using "C:\Program Files\ArcGIS\Dotnet\ESRI.ArcGIS.Geometry.dll"

namespace MCpp
{
public __gc class AOWrapper
{
public:
AOWrapper()
{
::CoInitialize(0);
}
~AOWrapper()
{
::CoUninitialize();
}
public:
int line_test()
{
IPointPtr ipPt1(CLSID_Point);
IPointPtripPt2(CLSID_Point);
ipPt1->PutCoords(1, 2);
ipPt2->PutCoords(2, 3);
ILinePtr ipLine(CLSID_Line);
for(long i = 0; i < = 10000000; i++)
{
ipLine->PutCoords(ipPt1,ipPt2);
}
return 0;
}
};
}
This function can be called from a .NET application using code like the following (remember to add a
reference to the .dll created previously):

[C#]

private void button1_Click(object sender, System.EventArgs e)

int tick1, tick2, tick3;

MCpp.AOWrapper w = new MCpp.AOWrapper();

tick1 = System.Environment.TickCount;

w.line_test();

tick2 = System.Environment.TickCount;

tick3 = tick2 - tick1;

System.Windows.Forms.MessageBox.Show(tick3.ToString());

[VB.NET]

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As


System.EventArgs) Handles Button1.Click

Dim tick1, tick2, tick3 As Integer

Dim w As MCpp.AOWrapper = New MCpp.AOWrapper()

tick1 = System.Environment.TickCount

For i = 0 To 10000000

pLine.PutCoords(p1, p2)

Next i

tick2 = System.Environment.TickCount

tick3 = tick2 - tick1

System.Windows.Forms.MessageBox.Show(tick3.ToString())

End Sub

Using the proxy class written in Managed C++, the average running time is improved
to approximately 5 seconds.

One disadvantage of using Managed C++ is that the assembly written in Managed C++ cannot use
the verification mechanism of .NET security, so it needs to run in a trusted environment. Future
releases of Visual C++ .NET may add additional functionality that may alter this situation.
The key to improving performance is to find the performance bottleneck of your application, which
may or may not be the interop time. For most ArcObjects applications, developing in .NET will not
significantly deteriorate the performance of your code. If you find the interop is indeed the
bottleneck, writing Managed C++ proxy classes for parts of your code could provide a solution.

Implementing cloning

Summary.NET objects are of two types: value and reference. Variables of value type objects hold
the object bits themselves and have "copy-on-assignment" behavior. Variables of reference (ref)
type are actually pointers to memory. That is, when you create a new variable of ref type and
assign it to an existing object, you are actually creating another pointer to the same memory. This
topic explains how to create a new copy of an object and hold it in a variable.

About implementing cloning


Cloning is the process of copying an object—the 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 following code example creates two
variables that point to the same object in memory.

[C#]

IPoint pointOne = new PointClass();

IPoint pointTwo = pointOne;

[VB.NET]

Dim pointOne As IPoint = New PointClass()

Dim pointTwo As IPoint = pointOne

See the following illustration of two variables that point to the same object in memory:

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. See the following code example:

[C#]

IClone clone = pointOne as IClone;

pointTwo = clone.Clone()as IPoint;


[VB.NET]

Dim clone As IClone

clone = Ctype(pointOne, IClone)

pointTwo = Ctype(clone.Clone(), IPoint)

See the following illustration of the IClone interface:

Cloning creates a new instance in memory.

Cloning in ArcGIS
The technique shown previously is used extensively in ArcGIS by ArcObjects classes that implement
the IClone interface.

For example, before the application passes an object to a property page, it clones the object. If the
OK or Apply button is clicked, the properties of the cloned object are set into the original object.
Another use of cloning in ArcObjects is by methods or properties that specifically return a copy of an
object, for example, the IFeature.ShapeCopy property.

You can find other examples of how cloning is used by searching the samples included in the ArcGIS
Developer Help.

Copying members: Values and object references


The details of the clone operation are encapsulated in the class implementation—the class regulates
which of its members should be copied and how they should be copied.

Each class that implements cloning decides how to clone itself.


Terminology used in this section—The original object will be referred to as the cloner; the object
that performs the cloning operation. The object resulting from the cloning process will be called the
clonee.
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. If the cloner copies only the object references to the clonee, it is sometimes known as a
shallow clone. If new instances of each referenced object are created, and the clonee's
members are set to reference these new objects, it is referred to as a deep clone. See the following
illustration that shows the 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 that of a graphic element. Both the geometry and symbol of
the graphic element are cloned; the geometry and symbol properties of the clonee are entirely
separate from the original object's 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 feature class. Every geometry has an object reference
indicating its coordinate system (for example, IGeometry.SpatialReference). Cloning the geometry
produces an object with a reference to the same underlying spatial reference object. In this case,
only the object reference is copied, as it is logical for all geometries in a feature class to hold a
reference to the same spatial reference object, as does the layer itself. The spatial reference 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 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, keep in mind that some members should not be directly copied at all
—a window handle (hWnd), handle device context (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 example, a workspace and feature class both have connection-specific information
and are not clonable; an overview window and a tool control 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 need to decide how you want to copy the
information contained in your class—whether shallow or deep cloning is most appropriate for each
member—and how to implement it.

Coding IClone
In general, there are two primary techniques to implement cloning. In the first, the cloner object
will create a new instance of itself, then copy all of its members onto it.

The second technique takes advantage of the object's persistence mechanism functionality by
temporarily saving the object to a memory stream (ObjectStream), then "rehydrating" it by loading
it back from the memory stream. This technique requires the object to implement interfaces
IPersist and IPersistStream. It is quite complicated to implement IPersistStream in .NET; therefore,
this technique is not covered in this topic.

In the Clone method, begin by creating a new instance of the class, which is the clonee. You can
then call IClone.Assign() to copy the properties of the cloner to the clonee. Last, return a reference
to the clonee from clone. The following code example is part of a sample Clonable object:

[C#]

public IClone Clone()

ClonableObjClass obj = new ClonableObjClass();

obj.Assign(this);

return (IClone)obj;

[VB.NET]

Public Function Clone() As IClone Implements IClone.Clone

Dim obj As ClonableObjClass = New ClonableObjClass()

obj.Assign(Me)

Return CType(obj, IClone)

End Function

Clone should create a new instance of the class.


The Assign method receives a reference to a second instance of the class, src—this is the clonee.
First, check src to see if it is pointing to a valid object—if not, raise the appropriate standard
Component Object Model (COM) error.
Assign should receive a valid instance of the class.
[C#]

public void Assign(IClone src)

//1. Make sure src is pointing to a valid object.

if (null == src)

throw new COMException("Invalid object.");

//2. Verify the type of src.

if (!(src is ClonableObjClass))

throw new COMException("Bad object type.");

//3. Assign the properties of src to the current instance.

ClonableObjClass srcClonable = (ClonableObjClass)src;

m_name = srcClonable.Name;

m_version = srcClonable.Version;

m_ID = srcClonable.ID;

//Use shallow cloning (use a reference to the same member object).

m_spatialRef = srcClonable.SpatialReference)

[VB.NET]

Public Sub Assign(ByVal src As IClone) Implements IClone.Assign

'1. Make sure src is pointing to a valid object.

If Nothing Is src Then

Throw New COMException("Invalid object.")

End If
'2. Verify the type of src.

If Not (TypeOf src Is ClonableObjClass) Then

Throw New COMException("Bad object type.")

End If

'3. Assign the properties of src to the current instance.

Dim srcClonable As ClonableObjClass = CType(src, ClonableObjClass)

m_name = srcClonable.Name

m_version = srcClonable.Version

m_ID = srcClonable.ID

'Use shallow cloning (use a reference to the same member object).

m_spatialRef = spatialRef = srcClonable.SpatialReference)

End Sub

The cloner copies values from the clonee. See the following illustration:
The previous Assign code shows a shallow clone of the SpatialReference property. If
SpatialReference is another object reference, you can perform a deep clone. If the object itself
supports IClone, this is straightforward. See the following code example:

[C#]

IClone cloned = srcClonable.SpatialReference as IClone;

if (null != cloned)

m_spatialRef = (ISpatialReference)cloned.Clone();

[VB.NET]

Dim cloned As IClone = CType(srcClonable.SpatialReference, IClone)

If Not cloned Is Nothing Then


m_spatialRef = CType(cloned, ISpatialReference)

End If

If the member object does not support IClone, you must create a new object and set its properties
from the existing property of the source object, scr.

Consider whether it is more appropriate to copy only an object reference (for example, all the
geometries of a feature class hold a reference to the same spatial reference), clone the object
reference, or leave the member uncopied to be set by the client code as appropriate.

When coding the Assign method, 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
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.

Guaranteeing deep cloning


To verify deep cloning of your object's ref members, consider using the ObjectCopy object.
It provides a mechanism to duplicate an object using an object's persistence mechanism
(IPersistStream). The object's state is written to a temporary stream, then rehydrated from that
stream into a new instance of the object. This process is also known as a deep clone since an object
will also duplicate all subobjects it contains. Even if the object supports IClone, you may still want
to use ObjectCopy since it does a full copy, or deep clone, of the object. See the following code
example:

To use ObjectCopy to deep clone an object, the object must implement the IPersistStream interface.

[C#]

//Deep clone the spatial reference using an ObjectCopy.

if (null == srcClonable.SpatialReference)

m_spatialRef = null;

else

IObjectCopy objectCopy = new ObjectCopyClass();

object obj = objectCopy.Copy((object)srcClonable.SpatialReference);

m_spatialRef = (ISpatialReference)obj;

}
[VB.NET]

'Deep clone the spatial reference using an ObjectCopy.

If Nothing Is srcClonable.SpatialReference Then

m_spatialRef = Nothing

Else

Dim objectCopy As IObjectCopy = New ObjectCopyClass()

Dim obj As Object = objectCopy.Copy(CObj(srcClonable.SpatialReference))

m_spatialRef = CType(obj, ISpatialReference)

End If

The IsEqual method should compare the cloner (this) and the clonee (other) to see if all the
members are equal in value; it returns true if all the members are equal. See the following code
example:

[C#]

public bool IsEqual(IClone other)

//1. Make sure the "other" object is pointing to a valid object.

if (null == other)

throw new COMException("Invalid object.");

//2. Verify the type of "other."

if (!(other is ClonableObjClass))

throw new COMException("Bad object type.");

ClonableObjClass otherClonable = (ClonableObjClass)other;

//Test that all the object's properties are the same.

if (otherClonable.Version == m_version && otherClonable.Name == m_name


&&

otherClonable.ID == m_ID &&


((IClone)otherClonable.SpatialReference).IsEqual

((IClone)m_spatialRef))

)return true;
return false;

[VB.NET]

Public Function IsEqual(ByVal other As IClone) As Boolean Implements


IClone.IsEqual

'1. Make sure that "other" is pointing to a valid object.

If Nothing Is other Then

Throw New COMException("Invalid object.")

End If

'2. Verify the type of "other."

If Not (TypeOf other Is ClonableObjClass) Then

Throw New COMException("Bad object type.")

End If

Dim otherClonable As ClonableObjClass = CType(other, ClonableObjClass)

'Test that all the object's properties are the same.

If otherClonable.Version = m_version AndAlso _

otherClonable.Name = m_name AndAlso _

otherClonable.ID = m_ID AndAlso _

CType(otherClonable.SpatialReference,
IClone).IsEqual(CType(m_spatialRef, IClone)) Then Return True

Return True

End If

Return False

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. Remember
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 values. You may decide that two IColor members
are equal if they have the same red, green, blue (RGB) value, even though one is an RGB color and
one is a cyan, magenta, yellow, and black (CMYK) color.
To implement IsIdentical, compare the interface pointers to see if the cloner (this) and the clonee
(other) point to the same underlying object in memory. See the following code example:

[C#]

public bool IsIdentical(IClone other)

//1. Make sure the "other" object is pointing to a valid object.

if (null == other)

throw new COMException("Invalid object.");

//2. Verify the type of "other."

if (!(other is ClonableObjClass))

throw new COMException("Bad object type.");

//3. Test if the other is "this."

if ((ClonableObjClass)other == this)

return true;

return false;

[VB.NET]

Public Function IsIdentical(ByVal other As IClone) As Boolean Implements


IClone.IsIdentical

'Make sure that other is pointing to a valid object.

If Nothing Is other Then

Throw New COMException("Invalid object.")

End If

'Verify the type of other.

If Not (TypeOf other Is ClonableObjClass) Then

Throw New COMException("Bad object type.")

End If
'Test if the other is this.

If CType(other, ClonableObjClass) Is Me Then

Return True

End If

Return False

End Function

IsIdentical should compare interface pointers to see if they reference the same underlying object.
See the following illustration:

Implementing persistence

SummaryThis document reviews the technique of persistence of custom ArcObjects implemented


using .NET.

About implementing persistence


Persistence is a general term that refers 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
Component Object Model (COM) interfaces. Prior to structured storage, only a single file pointer was
used to access a file. However, in structured storage, a compound file model is used, whereby each
file contains storage objects and streams. Storage objects provide structure (for example, folders on
your operating system) and can contain other storage and stream objects. Stream objects provide
storage (for example, traditional files) and 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 system; it 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 can 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-
geographic information system (GIS) data.

Persistence in ArcGIS
Structured storage interfaces specified by COM are implemented extensively throughout the ArcGIS
framework. Understanding when persistence is used in the ArcGIS framework will help you to
implement correct persistence behavior in classes you create. The following sections explain when
to implement persistence and 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 ubiquitous; not every
object will always be given the opportunity to persist itself.

Compound document structure


ArcGIS applications use the compound document structure to store documents, such as map
documents, globe documents, map and globe templates, normal templates, and so on. All the
objects currently running in a document or template are persisted to streams in the compound file
when the document is saved.

For example, in a map document, when a user clicks Save in ArcMap, the MxApplication creates
streams as required, associates them with the existing .mxd file (if the document has previously
been saved), then requests the document to persist itself to these streams. If there are changes to
the normal template or map template, this process is repeated for the appropriate .mxt file. This
process allows the current state of a document to be re-created when the file is reopened.

ArcMap, for example, persists many items. Notable areas that may include custom objects are
described as follows:

• Map collection—Each map persists its layers, symbology, graphics, current extent, spatial
reference, and so on. This may include custom layers, renderers, symbols, elements, or
other map items.

• Page layout, map frames, map surrounds, layout of items, and so on—This may include
custom map surrounds or frames.

• Visible table of contents (TOC) views and their state—This may include a custom TOC view.

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

• Registered extensions and their state—This may include custom extensions.

• Current DataWindows, their type, location, and contents—This may include a custom
DataWindow.

• List of styles currently referenced by the StyleGallery; items are stored in a style by using
persistence—This can include a custom StyleGalleryItem or StyleGalleryClass.

Starting in ArcGIS 9.1, you can save map documents so you can open and work with them in
previous versions of ArcGIS. See the Version compatibility and Version compatibility consistency
sections in this topic 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 your class needs to implement
persistence and implement correct persistence behavior if required.

Persistent classes
When an object is asked to persist itself, it writes 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 is
most likely to 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 persist. This 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.
See the following illustration:

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 create, objects are persisted to one of the streams created by the
framework—for example, ArcMap in the case of ArcGIS Desktop and the MapDocument class in the
case of ArcGIS Engine—it is unlikely you need to create a new storage or stream.

Loaded 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 persistence—no errors are raised if it does not—it depends on
whether 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 class identifiers (CLSIDs).

The application object creates a separate stream for the persistence of each extension and 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 the following section for more information about
ObjectStreams.
ObjectStreams
An object's state is not always defined by value types; you have already seen how a map
document 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 is called to persist,
two separate copies of the layer are persisted in different sections of the stream. This bloats the file
size and also corrupts 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 creates a stream for the persistence. It
also creates an ObjectStream and associates 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 an object is encountered, it is persisted in the usual manner. If the same object is
encountered again, the ObjectStream ensures that instead of persisting the object a second time, a
reference to the existing saved object is stored. See the following illustration:

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 a persistence class


To create a persistable class, implement either IPersist and IPersistStream or
IPersistVariant. IPersistStream and IPersistVariant specify the following 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 IPersistStream.Save and


IPersistVariant.Save methods.

• Retrieve data from a stream and set the members of an object from that data using the
IPersistStream.Load and IPersistVariant.Load methods.
Implementing IPersistStream in .NET is an advanced technique and is not discussed in this
document. Instead, this document discusses the implementation of IPersistVariant. In any case, 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 the
ID). 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 class to be created. At this
point, the rest of the persisted data can be loaded into the new instance of the class.

If you want to implement version-specific persistence code, see Version compatibility for more
information.

All code sections in this document are taken from the Triangle graphic element sample.

What you need to save


When you implement a persistent class, the decision of what constitutes the persistent state for
your class is yours to make; exactly 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
This interface was specifically designed for use by non C++/VC++ programmers. See the following
code example:

[C#]

public sealed class TriangleElementClass: � � � IPersistVariant � � �

[VB.NET]

Public NotInheritable Class TriangleElementClass

Implements …, IPersistVariant…

In the ID property, create a unique identifier (UID) and set the object to the fully qualified class
name of your class, or alternatively, use your component's class identifier (CLASSID/globally unique
identifier [GUID]). See the following code example:

[C#]

public UID ID

get

UID uid = new UIDClass();

uid.Value = "{" + TriangleElementClass.CLASSGUID + "}";

return uid;
}

[VB.NET]

Public ReadOnly Property ID() As UID Implements IPersistVariant.ID

Get

Dim uid As UID = New UIDClass()

uid.Value = "{" & TriangleElementClass.CLASSGUID & "}"

Return uid

End Get

End Property

A basic implementation of Save and Load is shown in the following code example:

[C#]

public void Save(IVariantStream Stream)

Stream.Write(m_size);

Stream.Write(m_elementType);

Stream.Write(m_pointGeometry);

� � �

public void Load(IVariantStream Stream)

m_size = (double)Stream.Read();

m_elementType = (string)Stream.Read();

m_pointGeometry = Stream.Read()as IPoint;

...

[VB.NET]
Public Sub Save(ByVal Stream As IVariantStream) Implements
IPersistVariant.Save

Stream.Write(m_size)

Stream.Write(m_elementType)

Stream.Write(m_pointGeometry)

End Sub

Public Sub Load(ByVal Stream As IVariantStream) Implements


IPersistVariant.Load

m_size = CDbl(Stream.Read())

m_elementType = CStr(Stream.Read())

m_pointGeometry = TryCast(Stream.Read(), IPoint)

End Sub

In the previous example, one double, one string, and one point are persisted (m_size,
m_elementType, m_pointGeometry).

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 that implements
IVariantStream. Using this interface, any value type or 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.
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, adapt your
implementation of IPersistVariant to identify the document version that your component is being
persisted to.

Within a call to load or save, you can determine the version of the document by casting to the
IDocumentVersion interface on the stream object as shown in the following code example:

[C#]

public void Save(IVariantStream Stream)

if (Stream is IDocumentVersion)
{

IDocumentVersion docVersion = (IDocumentVersion)Stream 'if

(docVersion.DocumentVersion ==
esriArcGISVersion.esriArcGISVersion83)

//Save object as 8.3 version of itself.

else

//Save object.

[VB.NET]

Public Sub Save(ByVal Stream As IVariantStream) Implements


IPersistVariant.Save

If TypeOf Stream Is IDocumentVersion Then

Dim docVersion As IDocumentVersion

docVersion = CType(Stream, IDocumentVersion)

If docVersion.DocumentVersion =
esriArcGISVersion.esriArcGISVersion83 Then

'Load object as 8.3 version of itself.

Else

'Load object.

End If

End If

If your code is installed on machines with an installation of ArcGIS prior to 9.1, you cannot
guarantee that the stream passed to the persistence methods will support IDocumentVersion. As
previously shown, always try to cast for this interface and take appropriate action if this interface is
not found. You may want to provide your own functions to discover the installed version of ArcGIS,
or you may want to rely on your internal persistence version number. For more information, see
Coding backward compatibility in persistence.

Techniques for persisting different data


The following sections give advice on persisting certain types of data to a stream for implementors
of IPersistVariant.
Persisting objects
If you are using IPersistVariant, 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 ObjectStream associated internally with the stream to persist the object. See the following code
example:

[C#]

Stream.Write(m_pointGeometry);

[VB.NET]

Stream.Write(m_pointGeometry)

The object is reloaded in a similar way. See the following code example:

[C#]

m_pointGeometry = Stream.Read()as IPoint;

[VB.NET]

m_pointGeometry = TryCast(Stream.Read(), IPoint)

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 following code example demonstrates how this technique can be used, where m_array is a
member of the class:

[C#]

int count = m_array.Count;

Stream.Write(count);

for (int i = 0; i < count; i++)

Stream.Write(m_array[i]);

[VB.NET]

Dim Count As Integer = m_array.Count


Stream.Write(Count)

Dim i As Integer

For i = 0 To Count - 1

Stream.Write(m_array(i))

Next

The array can now be initialized correctly in the Load method. See the following code example:

[C#]

int count = (int)Stream.Read();

for (int i = 0; i < count; i++)

m_array.Add(Stream.Read());

[VB.NET]

Dim Count As Integer = CInt(Stream.Read())

Dim i As Integer

For i = 0 To Count - 1

m_array.Add(Stream.Read())

Next

Instead of using a standard dynamic array, you can store object references in an ESRI Array class
and persist each of these references in the same way (the Array class is not persistable).

Persisting a PropertySet
You can make use of the PropertySet class to persist a class's member data, as this class is
persistable. Maximum efficiency is gained during a save if you already use the PropertySet to
internally store your class data.

Document versions and ObjectStreams


The Version compatibility and Coding Save A Copy functionality sections describe how to deal with
the persistence of 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 also
need to deal with the document version correctly.

All core ArcObjects components deal correctly with document version persistence—they do not
implement the IDocumentVersionSupportGEN interface but instead deal with this issue internally. If
you are persisting an object to an ObjectStream, all core ArcObjects components can therefore be
relied on to either persist correctly regardless of version or 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. Because streams are sequential, your code should not attempt to continue reading, as the
stream pointer is not 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.
Review the following Version compatibility section. You 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, through 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 continues to load the remainder of the
document but without the failed layer. 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 that can open a
document with a persisted version of your component.

Version compatibility
If you develop a new version of a persistable component, it is likely that you need to persist
additional state information; this means you need to change the persistence signature of your class.
However, your component can 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
allows 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 following version compatibility model of ArcGIS in
mind:

• Backward 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 created with an earlier version of ArcGIS.

• Forward compatibility—It is possible to write forward-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 forward compatibility (this is not generally a
requirement for your components), from ArcGIS 9.1 onward 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 prior to that with which the document was
created. At ArcGIS 9.2, you can save to ArcGIS 8.3 or ArcGIS 9 and 9.1. ArcGIS 9.1 map
documents are directly compatible with ArcGIS 9, so there is no option to save them to version 9
specifically. See the following illustration:
If your component works without recompilation with both the current and previous ArcGIS 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
IDocumentVersionSupportGEN. This interface allows you to provide an alternative object. For more
information, see Coding Save A Copy functionality.

If your object can be saved to a previous version of ArcGIS but you may need to account for this in
your persistence code, adapt your implementation of IPersistVariant to identify the version being
persisted to and make any necessary changes. For more information, see Identifying the document
version.

Coding backward compatibility in persistence


You will now look at an example of creating a backward compatible class by creating two different
versions of the class. The following code example is built step by step, showing how to code the
persistence methods each time. You will create a custom graphic element that draws an equilateral
triangle element on the map. For more information, see Triangle graphic element.

Version 1
For the first version of your triangle element, you need to store the member variables in the
following code example:

[C#]

private double m_size = 20.0;

private double m_scaleRef = 0.0;

private esriAnchorPointEnum m_anchorPointType =


esriAnchorPointEnum.esriCenterPoint;

private bool m_autoTrans = true;

private string m_elementType = "TriangleElement";


private string m_elementName = string.Empty;

private ISpatialReference m_nativeSR = null;

private ISimpleFillSymbol m_fillSymbol = null;

private IPoint m_pointGeometry = null;

private IPolygon m_triangle = null;

[VB.NET]

Private m_size As Double = 20.0

Private m_scaleRef As Double = 0.0

Private m_anchorPointType As esriAnchorPointEnum =


esriAnchorPointEnum.esriCenterPoint

Private m_autoTrans As Boolean = True

Private m_elementType As String = "TriangleElement"

Private m_elementName As String = String.Empty

Private m_nativeSR As ISpatialReference = Nothing

Private m_fillSymbol As ISimpleFillSymbol = Nothing

Private m_pointGeometry As IPoint = Nothing

Private m_triangle As IPolygon = Nothing

Now implement IPersistVariant, as a graphic element must be persistable. Before coding the
persistence members of the TriangleElement class, add the c_Version private constant. Use this
constant throughout the persistence code to store the version of the class. Set the constant to 1, as
this is the first version of the class. See the following code example:

[C#]

private const int c_Version = 1;

[VB.NET]

Private Const c_Version As Integer = 1

The use of this value is the key to version compatibility; 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. See the following code example:

[C#]

public void Save(IVariantStream Stream)

Stream.Write(c_Version);
[VB.NET]

Public Sub Save(ByVal Stream As IVariantStream) Implements


IPersistVariant.Save

Stream.Write(c_Version)

Then you can write the class state to the stream. See the following code example:

[C#]

Stream.Write(m_size);

Stream.Write(m_scaleRef);

Stream.Write(m_anchorPointType);

Stream.Write(m_autoTrans);

Stream.Write(m_elementType);

Stream.Write(m_elementName);

Stream.Write(m_nativeSR);

Stream.Write(m_fillSymbol);

Stream.Write(m_pointGeometry);

Stream.Write(m_triangle);

[VB.NET]

Stream.Write(m_size)

Stream.Write(m_scaleRef)

Stream.Write(m_anchorPointType)

Stream.Write(m_autoTrans)

Stream.Write(m_elementType)

Stream.Write(m_elementName)

Stream.Write(m_nativeSR)

Stream.Write(m_fillSymbol)

Stream.Write(m_pointGeometry)

Stream.Write(m_triangle)

End Sub

In the Load method, read the version number of the persisted class and store this value in the ver
local variable. If this version number indicates a version of the persisted class that is newer than the
current version of the class (c_Version), the class does not know how to load the persisted
information correctly. If ver = 0, there is an error somewhere, as the minimum expected value is 1.
Both cases are errors and may cause a corrupt stream. In these cases, raise an exception back to
the calling object. See the following code example:

[C#]

public void Load(IVariantStream Stream)

int ver = (int)Stream.Read();

if (ver > c_Version || ver <= 0)

throw new Exception("Wrong version!");

[VB.NET]

Public Sub Load(ByVal Stream As IVariantStream) Implements


IPersistVariant.Load

Dim ver As Integer = CInt(Fix(Stream.Read()))

If ver > c_Version OrElse ver <= 0 Then

Throw New Exception("Wrong version!")

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. See the following code example:

[C#]

InitMembers();

[VB.NET]

InitMembers()

Now you can read the persisted class state and set the members of the current object after
checking that the saved persistence version is the version you expect (this check is useful later
when you produce a new version of your component). See the following code example:

[C#]

if (ver == 1)

m_size = (double)Stream.Read();

m_scaleRef = (double)Stream.Read();

m_anchorPointType = (esriAnchorPointEnum)Stream.Read();
m_autoTrans = (bool)Stream.Read();

m_elementType = (string)Stream.Read();

m_elementName = (string)Stream.Read();

m_nativeSR = Stream.Read()as ISpatialReference;

m_fillSymbol = Stream.Read()as ISimpleFillSymbol;

m_pointGeometry = Stream.Read()as IPoint;

m_triangle = Stream.Read()as IPolygon;

[VB.NET]

If ver = 1 Then

m_size = CDbl(Stream.Read())

m_scaleRef = CDbl(Stream.Read())

m_anchorPointType = CType(Stream.Read(), esriAnchorPointEnum)

m_autoTrans = CBool(Stream.Read())

m_elementType = CStr(Stream.Read())

m_elementName = CStr(Stream.Read())

m_nativeSR = TryCast(Stream.Read(), ISpatialReference)

m_fillSymbol = TryCast(Stream.Read(), ISimpleFillSymbol)

m_pointGeometry = TryCast(Stream.Read(), IPoint)

m_triangle = TryCast(Stream.Read(), IPolygon)

End If

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 TriangleElement graphic
elements.

You can use the TriangleElementTool included in the project to add new triangle elements to a
document.

Version 2
You are now asked to add functionality to allow rotatation of the triangle elements. To achieve these
requirements, you must adapt your component. Add a class member to keep the element's rotation
and apply the rotation to the element when building the triangle, as well as in the implementation of
the ITransform2D interface. See the following code example:

[C#]

private double m_rotation = 0.0;


[VB.NET]

Private m_rotation As Double = 0.0

As the data that needs to be persisted has now changed, increment the persist version number for
the class by one. See the following code example:

[C#]

private const int c_Version = 2;

[VB.NET]

Private Const c_Version 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 the Load
method. See the following code example:

[C#]

public void Save(IVariantStream Stream)

Stream.Write(c_Version);

� � �

//New addition in version 2.

Stream.Write(m_rotation);

[VB.NET]

Public Sub Save(ByVal Stream As IVariantStream) Implements


IPersistVariant.Save

Stream.Write(c_Version)

'New addition in version 2.

Stream.Write(m_rotation)

End Sub

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

Method InitMembers() set a default rotation value in cases where the object has been read as
version 1.
See the following code example:
[C#]

public void Load(IVariantStream Stream)

int ver = (int)Stream.Read();

if (ver > c_Version || ver <= 0)

throw new Exception("Wrong version!");

InitMembers();

� � �

if (ver == 2)

m_rotation = (double)Stream.Read();

[VB.NET]

Public Sub Load(ByVal Stream As IVariantStream) Implements


IPersistVariant.Load

Dim ver As Integer = CInt(Fix(Stream.Read()))

If ver > c_Version OrElse ver <= 0 Then

Throw New Exception("Wrong version!")

End If

InitMembers()

If ver = 2 Then

m_rotation = CDbl(Stream.Read())

End If

End Sub
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 TriangleElement class. At this point, if the new version of
the component encounters an older persisted version, it can load from the persisted data.

Once the document is saved again (by the version 2 component), the persisted version will be
version 2.

Obsolete persisted data


In many cases, a new component version requires adding data to a persistence pattern. Sometimes
a new version of a component may no longer need to save certain data to the stream.

In these cases, if your new component encounters the older version persisted to a stream, you
should always read the obsolete data values; otherwise, the stream pointer is left at the wrong
location and the next value is not read correctly. You can discard the obsolete values once read and
save only the required data in the new Save method. See the following illustration:

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 enables old components to load the persisted object.

The persistence version number written at the beginning of the Save method should account for the
persistence pattern 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, then the Save and Load events 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 want to implement
IDocumentVersionSupportGEN. This interface specifies the following pieces of functionality:

• Allows a component to indicate whether it can be persisted to a particular version of an


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 cast to IDocumentVersionSupportGEN for a given persistable object, it assumes


that the object can be persisted and unchanged to any version of an ArcGIS document.

For example, in a situation where a custom symbol is applied to a layer in a document at ArcGIS 9.2
and the user chooses to save a copy of the document to an ArcGIS 8.3 document, the persistence
process for the symbol follows these general steps to create the version-specific document:

1. When ArcMap attempts to persist the symbol object, it attempts to cast


to IDocumentVersionSupportGEN to determine if the symbol can be saved to an 8.3
document.
2. If the cast fails, ArcMap calls the persistence methods of the symbol as normal, assuming
the symbol can be persisted to any version of ArcGIS.

3. If the cast succeeds, ArcMap then calls the


IDocumentVersionSupportGEN.IsSupportedAtVersion method, passing in a value indicating
the ArcGIS version required.

• If IsSupportedAtVersion returns true, ArcGIS calls the persistence methods of the


symbol as normal.

• If IsSupportedAtVersion returns false, ArcGIS calls the


IDocumentVersionSupportGEN.ConvertToSupportedObject method on the symbol.
The symbol then creates a suitable alternative symbol object that can be persisted
to the ArcGIS document version specified. This alternative symbol object is then
returned from ConvertToSupportedObject, and ArcGIS replaces object references
to the original symbol with references to this alternative symbol. The following
illustration shows this last situation:
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 return
true from IsSupportedAtVersion, although in this case, you do not need to implement the interface
at all.

If, however, your component relies on the presence of core objects or functionality that exist only
from a certain version onward, return false to indicate that the object cannot be used in a previous
version's document (for example, the triangle element example previously explained). You can
prevent the element from being saved as it is to an 8.3 document using code like that shown in the
following code example:

[C#]

public bool IsSupportedAtVersion(esriArcGISVersion docVersion)

//Support all versions except 8.3.

if (esriArcGISVersion.esriArcGISVersion83 == docVersion)

return false;

else

return true;

}
[VB.NET]

Public Function IsSupportedAtVersion(ByVal docVersion As esriArcGISVersion)


As Boolean Implements IDocumentVersionSupportGEN.IsSupportedAtVersion

'Support all versions except 8.3.

If esriArcGISVersion.esriArcGISVersion83 = docVersion Then

Return False

Else

Return True

End If

End Function

To allow users to save a copy of a document with the custom triangle element as an 8.3 document,
you can use code like that shown in the following example to create a basic marker element that
uses a triangular character marker symbol as an alternative to the custom triangle element and
return this from ConvertToSupportedObject. (This may not be an appropriate solution for every
custom graphics element, but the principle can be applied to similar code to create an object
appropriate to your own customizations).

[C#]

public object ConvertToSupportedObject(esriArcGISVersion docVersion)

//In case of 8.3, create a character marker element and use a triangle
marker.

ICharacterMarkerSymbol charMarkerSymbol = new


CharacterMarkerSymbolClass();

charMarkerSymbol.Color = m_fillSymbol.Color;

charMarkerSymbol.Angle = m_rotation;

charMarkerSymbol.Size = m_size;

charMarkerSymbol.Font = ESRI.ArcGIS.ADF.Converter.ToStdFont(new Font(

"ESRI Default Marker", (float)m_size, FontStyle.Regular));

charMarkerSymbol.CharacterIndex = 184;

IMarkerElement markerElement = new MarkerElementClass();

markerElement.Symbol = (IMarkerSymbol)charMarkerSymbol;

IPoint point = ((IClone)m_pointGeometry).Clone()as IPoint;


IElement element = (IElement)markerElement;

element.Geometry = (IGeometry)point;

return element;

[VB.NET]

Public Function ConvertToSupportedObject(ByVal docVersion As


esriArcGISVersion) As Object Implements
IDocumentVersionSupportGEN.ConvertToSupportedObject

'In case of 8.3, create a character marker element and use a triangle
marker.

Dim charMarkerSymbol As ICharacterMarkerSymbol = New


CharacterMarkerSymbolClass()

charMarkerSymbol.Color = m_fillSymbol.Color

charMarkerSymbol.Angle = m_rotation

charMarkerSymbol.Size = m_size

charMarkerSymbol.Font = ESRI.ArcGIS.ADF.Converter.ToStdFont(New
Font("ESRI Default Marker", CSng(m_size), FontStyle.Regular))

charMarkerSymbol.CharacterIndex = 184

Dim markerElement As IMarkerElement = New MarkerElementClass()

markerElement.Symbol = CType(charMarkerSymbol, IMarkerSymbol)

Dim point As IPoint = TryCast((CType(m_pointGeometry, IClone)).Clone(),


IPoint)

Dim element As IElement = CType(markerElement, IElement)

element.Geometry = CType(point, IGeometry)

Return element

End Function

For every esriArcGISVersion for which you return false from IsSupportedAtVersion, you should
implement a suitable alternative in ConvertToSupportedObject. If you do not do this, you prevent
users from saving a copy of a document to that version, and the user may not receive any
information about why 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.
Responsibilities when implementing persistence
Review the following for a recap of your responsibilities when creating persistable components.

Controlling the data to persist


Remember that you control what is written to the stream. If your class needs to persist a reference
to another custom object, you have the following two options:

• Implement persistence on the other custom class and persist that class as you would any
other object.

• Alternatively, write the members of the secondary class to your persist stream for the
primary class. Although this 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.

ObjectStreams in extensions
As previously mentioned, a separate ObjectStream is created for each extension. This situation can
lead to problems for components that 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 results in two separate objects being persisted. When
the document is reloaded, the object you persisted is no longer 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 consistency


You must ensure that your components are consistent with the version compatibility used by
ArcGIS. Ensure as a minimum that your components are backward compatible—that a client (for
example, ArcMap) can open a document created with an earlier version of your component.

You should also consider implementing IDocumentVersionSupportGEN, if required, to ensure that


your component can be opened in a document that 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.

How to cast between interfaces

Summary.NET uses casting to jump from one interface to another in the same class. In a
Component Object Model (COM), this is a query interface (QI). Visual Basic (VB) .NET and C# cast
differently.

Casting in VB .NET
There are two types of casts: implicit and explicit. Implicit casts do not require additional syntax,
whereas explicit casts require cast operators. See the following code example:

[VB.NET]

'Implicit cast.

geometry = point

'Explicit cast.
geometry = CType(point, IGeometry)

geometry = DirectCast(point, IGeometry)

geometry = TryCast(point, IGeometry)

When casting between interfaces, it is acceptable to use implicit casts, because there is no chance
of data loss as when casting between numeric types. However, when casts fail, an exception
(System.InvalidCastException) is thrown. To avoid handling unnecessary exceptions, it is best to
test if the object implements both interfaces beforehand.

One of the recommended techniques is to use the TypeOf keyword, which is a comparison clause
that tests whether an object is derived from or implements a particular type, such as an interface.

The following code example performs an implicit conversion from an IPoint to an IGeometry only if
it is determined at runtime that the Point class implements IGeometry:

[VB.NET]

Dim point As New PointClass

Dim geometry As IGeometry

If TypeOf point Is IGeometry Then

geometry = point

End If

If you prefer using the Option Strict On statement to restrict implicit conversions, use the CType or
DirectCast function to make the cast explicit. The following code example adds an explicit cast to
the previous code example:

[VB.NET]

Dim point As New PointClass

Dim geometry As IGeometry

If TypeOf point Is IGeometry Then

geometry = CType(point, IGeometry)

End If

Alternatively, you can skip the TypeOf comparison and use TryCast; it returns Nothing when
conversion fails. See the following code example:

[VB.NET]

Dim point As New PointClass

Dim geometry As IGeometry = TryCast(point, IGeometry)

If geometry IsNot Nothing Then

Console.WriteLine(geometry.GeometryType.ToString())

End If
Casting in C#
In C#, the best method for casting between interfaces is to use the as operator. Using the as
operator is a better coding strategy than a straight cast, because it yields a null on a conversion
failure rather than raising an exception.

The first line in the following code example is a straight cast. This is an acceptable practice if you
are certain the object in question implements both interfaces. If the object does not implement the
interface you are attempting to get a handle to, .NET throws an exception. A safer model to use is
the as operator, which returns a null if the object cannot return a reference to the desired interface.

[C#]

IGeometry geometry = (IGeometry)point; //Straight cast.

IGeometry geometry = point as IGeometry; //As operator.

The following code example shows how to manage the possibility of a returned null interface handle
after an explicit cast:

[C#]

IPoint point = new PointClass();

IGeometry geometry = point as IGeometry;

if (geometry != null)

Console.WriteLine(geometry.GeometryType.ToString());

Alternatively, you can test if an object implements a certain interface at run time using the is
keyword before performing a straight cast. See the following code example:

[C#]

IPoint point = new PointClass();

if (point is IGeometry)

IGeometry geometry = (IGeometry)point;

Console.WriteLine(geometry.GeometryType.ToString());

About namespaces
Namespaces are a concept in .NET that allows objects to be organized hierarchically into logical
categories of related functionality regardless of the assembly in which they are defined. The interop
assemblies are named after the object library name minus the "esri" prefix. Using this convention,
for example, the object library esriCarto.olb corresponds to the assembly ESRI.ArcGIS.Carto.dll.

The classes in the interop assemblies (provided as part of the ArcObjects Developer Kit for .NET)
generally replicate the assembly name. For example, all the items defined in the
ESRI.ArcGIS.Carto.dll assembly belong to the namespace ESRI.ArcGIS.Carto. One notable exception
is the ESRI.ArcGIS.System assembly, which corresponds to the esriSystem.olb object library. To
avoid a conflict with the Microsoft System root namespace, the items in this assembly belong to the
ESRI.ArcGIS.esriSystem namespace.

To find out more about the contents of the ArcGIS interop assemblies, use the Contents tab and
navigate to the ArcObjects Library Reference section.

In addition to the assemblies that provide Component Object Model (COM) interop functionality for
the objects in the standard object libraries, there are also helper classes specifically designed for
use with ArcObjects in .NET. These helper classes are defined in
the assemblies ESRI.ArcGIS.ADF and ESRI.ArcGIS.ADF.Connection. They have a number of
namespaces, the most commonly used being ESRI.ArcGIS.ADF.BaseClasses,
ESRI.ArcGIS.ADF.CATIDs, and ESRI.ArcGIS.ADF.COMSupport.

Using namespaces
After you have added a reference to an assembly, you can make your code simpler and more
readable by adding directives that act as shortcuts when you declare or reference items specified in
the namespaces contained in the referenced assemblies. This allows you to use classes and other
types defined within the imported namespaces without having to fully qualify the class and type
names.

• In C#, use the "using" directive. See the following code example:

[C#]

using ESRI.ArcGIS.Carto;

• In VB.NET, use the "Imports" statement. See the following code example:

[VB.NET]

Imports ESRI.ArcGIS.Carto

Defining namespaces
You can organize your objects in namespaces as well. By default, the namespace is set to the name
of the project; in VB.NET, it is the Root Namespace.

In C#, you can set the default namespace in the project properties dialog box. All new items added
to the project will use the default namespace as shown in the following screen shot below:
C# class files have Namespace statements by default. This namespace declares the same
namespace as the Default Namespace. If you want a class in your project to have a different
namespace from the one defined by the Default Namespace, you can simply change the Namespace
statement in that class. See the following code example:

[C#]

namespace EsriSamples.ArcMapCommands

public class ZoomIn{}

In VB.NET, you can set the Root Namespace property to your namespace. All items in the project
will use this namespace. You can set the Root Namespace in the project properties dialog box as
shown in the following screen shot:
By default, Visual Basic (VB) class files do not have a Namespace statement to declare a
namespace; you have to add this manually if you want it. Be aware that if you have the Root
Namespace set and you also use a Namespace statement in a code file, the namespace defined in
the file gets appended to the Root Namespace.

In the following code example, the Root Namespace is set to EsriSamples and the namespace
declared in the VB class code file is ArcMapCommands, so the namespace for the ZoomIn class will
be EsriSamples.ArcMapCommands.

[VB.NET]

Namespace ArcMapCommands

' RootNamespace is set to EsriSamples.

<ComClass(ZoomIn.ClassId, ZoomIn.InterfaceId, ZoomIn.EventsId)> _

Public Class ZoomIn

...

End Class

End Namespace

The VB.NET ArcObjects samples do not use the Namespace statement; only the Root Namespace is
used.

About ProgIDs
In .NET, a ProgID (program identifier) is automatically generated for a class in the following format:
<namespace>.<class name>. You can override this and explicitly set the ProgID of your object by
using the ProgID attribute. It may be necessary to do this if your namespace is long; ProgIDs are
limited to 39 characters and cannot contain punctuation other than a period.

The following code example explicitly sets the ProgID to ZoomInCmd.ZoomIn:

[C#]

namespace EsriSamples

[Guid("97FDB5D7-41D8-4260-BF72-3EB2CDD36747")]
[ProgID("ZoomInCmd.ZoomIn")]

public class ZoomIn: ICommand

...

[VB.NET]

' RootNamespace is set to EsriSamples.

<ComClass(ZoomIn.ClassId, ZoomIn.InterfaceId, ZoomIn.EventsId), _

ProgID("ZoomInCmd.ZoomIn")> _

Public Class ZoomIn

...

Using ProgIDs
If you have used the ProgID attribute and your class is registered in a component category, the
<namespace>.<class name> is the identifier you will see in the Component Category Manager
instead of the ProgID. However, your ProgID will work when you need to use it for ArcObjects
methods that take a ProgID such as IUID.Value and IItemDef.ID. In the previous code example,
EsriSamples.ZoomIn is the identifier that appears in the Component Category Manager, but you
would use ZoomInCmd.ZoomIn as the ProgID when you need to use a ProgID programmatically.

How to register .NET components with COM

SummaryExtending the ArcGIS applications with custom .NET components requires that the .NET
classes are made available to the Component Object Model (COM) runtime by registering the
components in the COM registry. The three ways to perform this task are outlined in this topic. In
addition, if the component is to be used from a COM development environment, you may also want
to export a type library, which is also outlined in this topic.

Building the project on a development machine

• Use: On a development machine


When you want to register an assembly for COM interop on a development machine, the simplest
way is to open the project in Visual Studio 2005, ensure the project-level Register for COM Interop
property is set to true, then build the project.

To check a project's settings, click Project Properties from the Project menu, select the Build (C#) or
Compile (VB.NET) page, and select the Register for COM Interop check box.

Registering by command, using the Regasm utility

• Use: When testing on a different machine and you do not have an installation program

Sometimes you may want to register an assembly for COM interop without building the project; for
example, when first testing your component or if you do not have access to the assemblies source
code.

In this case, you can use the assembly registration utility Regasm that ships with the .NET
Framework Software Development Kit (SDK) or Visual Studio 2005. Regasm adds entries to the
registry, which allows a COM (unmanaged) application to consume .NET classes via COM interop.

Machines with Visual Studio 2005 or the freely available .NET Framework SDK will have this utility
installed and other machines may not; therefore, this is not a viable solution for general deployment
of your components.
Using the Regasm command line utility
Do the following to use the Regasm command line utility:

• If the target machine has Visual Studio installed, open the Visual Studio 2005 command
prompt (the ordinary command prompt will not have the appropriate environment variables
set to use this tool).

• If the target machine does not have Visual Studio installed but does have the .NET
Framework SDK installed, use the SDK command prompt by choosing Programs, then
Microsoft .NET Framework SDK v2.0. If you cannot find it, open a command prompt at the
path where Regasm is located. For example, the .NET Framework 2.0 default installation
path is C:\Windows\Microsoft.NET\Framework\v2.0.50727.

For Windows Vista, start the command prompt as an administrator by right-clicking the Command
Prompt icon and clicking the Run as Administrator option. This allows the Windows Registry to be
written to when you use the Regasm.exe utility.
The following example shows the command used to register the assembly (EditTools) with COM.
Give the full path to the assembly unless the current directory of the command prompt is the
directory where the assembly is located:

regasm EditTools.dll /codebase

The /codebase parameter is an optional parameter that adds information to the registry specifying
the path on disk of the assembly. If the component is not to be deployed to the global assembly
cache (GAC), this option will be required for ArcGIS to find your component successfully; if the
component is installed to the GAC, the option is not required. Regasm has many other options; for a
full list, type regasm /? or refer to the Microsoft Developer Network (MSDN) Web site.

Regasm can also be used to unregister assemblies from COM interop, as shown in the following
example:

regasm EditTools.dll /unregister

Using an installation program


• Use: When deploying to your user

Create an installation program that deploys your component and adds to the installation program an
automatic registration step that registers your component for COM interop.

Create installation programs by using third-party installation software or a Visual Studio setup
project. For an example of how to create a setup project for a component using WISE, see How to
deploy an application.

Type libraries for managed components

A type library (.tlb) can also be exported that contains information describing the types in the
assembly. You may want to generate a type library for a component if that component will be used
from another development environment, such as within the Visual Basic for Applications (VBA)
environment embedded with the ArcGIS applications. If you want to add custom commands and
tools using the Customize dialog box within the ArcGIS applications, generate a type library.

Visual Studio generates the .tlb file automatically if you have selected the Register for COM Interop
setting. If you do not have a .tlb for an existing component, one can be generated by using the
Regasm utility and the /tlb option. See the following:

regasm EditTools.dll /tlb:EditTools.tlb

How to register classes in COM component categories

SummaryMuch of the extensibility of ArcGIS relies on component object model (COM) categories;
for example, all ArcMap commands and tools are registered in the ESRI Mx Commands component
category. This article provides an overview of the different ways you can register a .NET
component in a particular category.

Additional Requirements

• Components must be registered with COM before they can be registered to


component categories.

Registering classes in COM component categories


There are a few different ways you can register a .NET component in a particular category:

• The simplest and recommended method is to add code to your .NET classes that
automatically registers the classes in a particular component category when the component
is registered with COM.
o By using the ArcGIS Component Category Registrar dialog box, which is part of the
ArcGIS Visual Studio IDE Integration Framework, you can quickly add code to your
classes, which automatically registers them to component categories. This dialog
box uses classes from the ESRI.ArcGIS.ADF assembly to help perform the
registrations.

• The Customize dialog box in ArcGIS applications can be used to add commands, tools, and
toolbars. By clicking the Add From File button on this dialog box, you can browse for the
type library (.tlb) file created for your customization and open it, at which point the ArcGIS
framework automatically adds the classes you select in the type library to the appropriate
component category.
o This method may be most useful if you want to use an existing compiled
component, which contains command items (commands, tools, and toolbars) but
does not have automatic registration code.

For .NET components, you must always select the type library, rather than the .dll file.

• Another option is to use the Component Categories Manager (Categories.exe). In this case,
you select the desired component category in the utility, browse for your type library, and
choose the appropriate class.

• This method may be most useful if you want to use an existing compiled component that
contains custom classes but does not have automatic registration code.

The actual actions performed are essentially the same, regardless of the method of category
registration you choose.

Working with component categories using ESRI.ArcGIS.ADF

About working with component categories using ESRI.ArcGIS.ADF


All custom ArcGIS components must be registered in the component category appropriate to their
intended context and function so that the host application can make their functionality available. For
example, all ArcMap commands and tools are registered in the ESRI Mx Commands component
category.

Component Object Model (COM) components created in .NET can have a self-registration
process. They are then automatically registered in the correct component category whenever the
dynamic-link library (DLL) is registered. The reverse case is also true: unregistering the DLL
removes the component category registry entry.

The .NET Framework contains two attribute classes, ComRegisterFunctionAttribute and


ComUnregisterFunctionAttribute (under System.Runtime.InteropServices), that allow you to specify
methods that are called whenever your component is being registered or unregistered. Both
methods are passed through the class identifier (CLSID) of the class currently being registered.
With this information, you can write code inside the methods to make the appropriate registry
entries or deletions. Registering a component in a component category requires that you also know
the component category’s unique ID (CATID).

To help the registration process, ESRI provides the Application Developer Framework (ADF)
assembly for .NET (ESRI.ArcGIS.ADF.dll), which contains the namespace ESRI.ArcGIS.ADF.CATIDs.
This namespace contains classes representing each of the ArcGIS component categories. Each class
knows its CATID and exposes static methods (Register and Unregister) for adding and removing
components. Registering your component becomes as easy as adding COM registration methods
with the appropriate attributes and passing the received CLSID to the appropriate static method.

The following code example shows a custom Pan tool that registers itself in the ESRI Mx Commands
component category:

[C#]

using ESRI.ArcGIS.ADF.CATIDs;

public sealed class PanTool: ESRI.ArcGIS.ADF.BaseClasses.BaseTool

[ComRegisterFunction()] static void Reg(string regKey)


{

MxCommands.Register(regKey);

[ComUnregisterFunction()] static void Unreg(string regKey)

MxCommands.Unregister(regKey);

� � �

[VB.NET]

Imports ESRI.ArcGIS.ADF.CATIDs

Public NotInheritable Class PanTool

Inherits ESRI.ArcGIS.ADF.BaseClasses.BaseTool

<ComRegisterFunction()> _

Public Shared Sub Reg(ByVal regKey As [String])

MxCommands.Register(regKey)

End Sub

<ComUnregisterFunction()> _

Public Shared Sub Unreg(ByVal regKey As [String])

MxCommands.Unregister(regKey)

End Sub

End Class

Writing multithreaded ArcObjects code


SummaryMultithreading allows an application to do more than one thing at a time within a single
process. This topic details what multithreading means in the context of the ArcObjects .NET
Software Developer Kit (SDK) as well as the rules that must be followed to properly integrate
threading into ArcObjects applications.

This topic does not attempt to give an introduction to multithreading or to teach multithreading
concepts, but rather to give practical solutions for daily ArcObjects programming problems that
involve multithreading.

Introduction to multithreading
Multithreading is generally used to improve the responsiveness of applications. This responsiveness
can be the result of real performance improvements or the perception of improved performance. By
using multiple threads of execution in your code, you can separate data processing and input/output
(I/O) operations from the management of your program's user interface (UI). This will prevent any
long data processing operations from reducing the responsiveness of your UI.

The performance advantages of multithreading come at the cost of increased complexity in the
design and maintenance of your code. Threads of an application share the same memory space, so
you must make sure that accessing shared data structures is synchronized to prevent the
application from entering into an invalid state or crashing. This synchronization is often called
concurrency control.

Concurrency control can be achieved at two levels: the object level and the application level. It can
be achieved at the object level when the shared object is thread safe, meaning that the object
forces all threads trying to access it to wait until the current thread accessing the object is finished
modifying the object’s state. Concurrency control can be done at the application level by acquiring
an exclusive lock on the shared object, allowing one thread at a time to modify the object’s state.
Be careful when using locks, as overuse of them will protect your data but can also lead to
decreased performance. Finding a balance between performance and protection requires careful
consideration of your data structures and the intended usage pattern for your extra threads.

When to use multithreading


There are two items to consider when building multithreaded applications: thread safety and
scalability. It is important for all objects to be thread safe, but simply having thread-safe objects
does not automatically mean that creating multithreaded applications is straightforward or that the
resulting application will provide improved performance.

The .NET Framework allows you to easily generate threads in your application; however, writing
multithreaded ArcObjects code should be done carefully. The underlying architecture of ArcObjects
is Component Object Model (COM). For that reason, writing multithreading ArcObjects applications
requires an understanding of both .NET multithreading and the threading model for COM.

Multithreading will not always make your code run faster; in many cases it will add extra
overhead and complexity that would eventually reduce the execution speed of your code.
Multithreading should only be used when the added complexity is worth the cost. A general rule of
thumb is that a task is suited to multithreading if it can be broken into different independent tasks.

ArcObjects threading model


All ArcObjects components are marked as single threaded apartment (STA). STAs are limited to one
thread each, but COM places no limit on the number of STAs per process. When a method call
enters an STA, it is transferred to the STA's one and only thread. Consequently, an object in an STA
will only receive and process one method call at a time, and every method call that it receives will
arrive on the same thread.

ArcObjects components are thread safe, and developers can use them in multithreaded
environments. For ArcObjects applications to run efficiently in a multithreaded environment, the
apartment threading model used by ArcObjects, Threads in Isolation, must be considered. This
model works by eliminating cross-thread communication. All ArcObjects references within a thread
should only communicate with objects in the same thread.
For this model to work, the singleton objects at ArcGIS 9.x were designed to be singletons per
thread and not singletons per process. The resource overhead of hosting multiple singletons in a
process is outweighed by the performance gain of stopping the cross-thread communication that
would occur if the singleton were created in one thread, then accessed from the other threads.

As a developer of the extensible ArcGIS system, all objects, including those you write, must adhere
to this rule for the Threads in Isolation model to work. If you are creating singleton objects as part
of your development, you must ensure that these objects are singletons per thread, not per
process.

To be successful using ArcObjects in a multithreaded environment, programmers must follow the


Threads in Isolation model while writing their multithreaded code in such a way as to avoid
application failures such as deadlock situations, negative performance due to marshalling, and
other unexpected behavior.

Multithreading scenarios
Although there are a number of ways to implement multithreading applications, the following are a
few of the more common scenarios that developers encounter.

The ArcObjects .NET SDK includes several threading samples, referenced in the following code
example, that cover the scenarios described in this topic. The samples demonstrate solutions for
real-life problems while showing the best programming practices. While these samples use
multithreading as part of a solution for a given problem, in some you will find that
the multithreading is just an architectural aspect of a broader picture.

Running lengthy operations on a background thread


When you are required to run a lengthy operation, it is convenient to allow the operation to run on a
background thread while leaving the application free to handle other tasks and keep the UI
responsive. Some examples of such operations include iterating through a FeatureCursor to load
information into a DataTable and performing a complex topological operation while writing the result
into a new FeatureClass.

To accomplish this task, keep in mind the following points:

• According to the Thread in Isolation model, you cannot share ArcObjects components
between threads. Instead, you will need to take advantage of the fact that singleton objects
are "per thread" and, in the background thread, instantiate all factories required to open
FeatureClasses, create new FeatureClasses, set spatial references, and so on.

• All information passed to the thread must be in the form of simple types or managed types.

• In cases where you must pass ArcObjects components from the main thread into a worker
thread, serialize the object into a string, pass the string to the target thread, and
deserialize the object back. For example, you can use XmlSerializerClass to serialize an
object, such as workspace connection properties (an IPropertySet), into a string; pass the
string with the connection properties to the worker thread; and deserialize the connection
properties on the worker thread using the XmlSerializerClass. This way, the connection
properties object are created on the background thread, and cross-apartment calls are
avoided.

• While running the background thread, you can report the task progress onto a UI dialog
box. This is covered in more detail in the Updating the UI from a background thread section
of this topic.

The following code example, an excerpt from the RSS weather layer sample, demonstrates a
background thread that is used to iterate through a FeatureCursor and populate a DataTable that
will is used later in the application. This keeps the application free to run without waiting for the
table to be populated.

[C#]
// Generate the thread that populates the locations table.

Thread t = new Thread(new ThreadStart(PopulateLocationsTableProc));

// Mark the thread as a single threaded apartment (STA) to efficiently run


ArcObjects.

t.SetApartmentState(ApartmentState.STA);

// Start the thread.

t.Start();

/// <summary>

/// Load the information from the MajorCities feature class to the
locations table.

/// </summary>

private void PopulateLocationsTableProc()

//Get the ArcGIS path from the registry.

RegistryKey key =
Registry.LocalMachine.OpenSubKey(@"SOFTWARE\ESRI\ArcGIS");

string path = Convert.ToString(key.GetValue("InstallDir"));

//Open the feature class. The workspace factory must be instantiated


since it is a singleton per-thread.

IWorkspaceFactory wf = new ShapefileWorkspaceFactoryClass()as


IWorkspaceFactory;

IWorkspace ws = wf.OpenFromFile(System.IO.Path.Combine(path, @

"Samples Net\Data\USZipCodeData"), 0);

IFeatureWorkspace fw = ws as IFeatureWorkspace;

IFeatureClass featureClass = fw.OpenFeatureClass(m_sShapefileName);

//Map the name and ZIP code fields.

int zipIndex = featureClass.FindField("ZIP");

int nameIndex = featureClass.FindField("NAME");

string cityName;

long zip;
try

//Iterate through the features and add the information to the


table.

IFeatureCursor fCursor = null;

fCursor = featureClass.Search(null, true);

IFeature feature = fCursor.NextFeature();

int index = 0;

while (null != feature)

object obj = feature.get_Value(nameIndex);

if (obj == null)

continue;

cityName = Convert.ToString(obj);

obj = feature.get_Value(zipIndex);

if (obj == null)

continue;

zip = long.Parse(Convert.ToString(obj));

if (zip <= 0)

continue;

//Add the current location to the location table.

//m_locations is a DataTable that contains the cities and ZIP


codes.

//It is defined in the full code before this excerpt starts.

DataRow r = m_locations.Rows.Find(zip);

if (null == r)

r = m_locations.NewRow();

r[1] = zip;
r[2] = cityName;

lock(m_locations)

m_locations.Rows.Add(r);

feature = fCursor.NextFeature();

index++;

//Release the feature cursor.

Marshal.ReleaseComObject(fCursor);

catch (Exception ex)

System.Diagnostics.Trace.WriteLine(ex.Message);

Implementing stand-alone ArcObjects applications


As stated on the Microsoft Developer Network (MSDN) Web site, "In the .NET Framework version
2.0, new threads are initialized as ApartmentState.MTA if their apartment state has not been set
before they are started. The main application thread is initialized to ApartmentState.MTA by default.
You can no longer set the main application thread to ApartmentState.STA by setting the
Thread.ApartmentState property on the first line of code. Use the STAThreadAttribute instead."

As an ArcObjects developer, this means that if your application is not initialized as a single threaded
application, the .NET framework will create a special single threaded apartment (STA) thread for all
ArcObjects since they are marked as STA. This will cause a thread switch to this thread on each call
from the application to ArcObjects. In turn, this forces the ArcObjects components to marshall each
call, and eventually it may be about 50 times slower for a call to the COM component. Fortunately,
this can be avoided by simply marking the main function as [STAThread].

The following code example marks a console application as STA:

[C#]

namespace ConsoleApplication1
{

class Program

[STAThread] static void Main(string[] args)

// ...

If you create a Windows Application using the VS2005 project wizard, it automatically puts the
[STAThread] on the main function for you.
Using managed ThreadPool and BackGroundWorker threads
Thread pool threads are background threads. Thread pooling enables you to use threads more
efficiently by providing your application with a pool of worker threads that are managed by the
system. The advantage of using a thread pool over creating a new thread for each task is that
thread creation and destruction overhead is negated, which can result in better performance and
better system stability.

However, by design all ThreadPool threads are in the multithreaded apartment (MTA) and
therefore should not be used to run ArcObjects which are single-threaded apartment.
To work around this problem you have a few options. One is to implement a dedicated ArcObjects
thread that is marked as STAThread and to delegate each of the calls from the MTA threads to this
dedicated ArcObjects thread. Another solution is to use an implementation of a custom STA thread
pool, such as an array of threads marked as STAThread to run ArcObjects.

The following code example—an excerpt from the Multithreaded raster subset sample—
demonstrates using an array of STAThread threads to subset a RasterDataset, using a different
thread to subset each raster band:

[C#]

/// <summary>

/// Class used to pass the task information to the working thread.

/// </summary>

public class TaskInfo

... public TaskInfo(int BandID, ManualResetEvent doneEvent)

m_bandID = BandID;

m_doneEvent = doneEvent;

}
...

... public override void OnMouseDown(int Button, int Shift, int X, int Y)

...

// Run the subset thread that will spin off separate subset tasks. By
default, this thread will run as MTA.

// This is needed to use WaitHandle.WaitAll(), the call must be made

// from MTA.

Thread t = new Thread(new ThreadStart(SubsetProc));

t.Start();

/// <summary>

/// Main subset method.

/// </summary>

private void SubsetProc()

...

//Create a dedicated thread for each band of the input raster.

//Create the subset threads.

Thread[] threadTask = new Thread[m_intBandCount];

//Each thread will subset a different raster band.

//All information required for subsetting the raster band will be


passed to the task by the user-defined class TaskInfo.

for (int i = 0; i < m_intBandCount; i++)

TaskInfo ti = new TaskInfo(i, doneEvents[i]);

...

// Assign the subsetting thread for the rasterband.


threadTask[i] = new Thread(new
ParameterizedThreadStart(SubsetRasterBand));

// Note the STA apartment that is required to run ArcObjects.

threadTask[i].SetApartmentState(ApartmentState.STA);

threadTask[i].Name = "Subset_" + (i + 1).ToString();

// Start the task and pass the task information.

threadTask[i].Start((object)ti);

...

// Wait for all threads to complete their task…

WaitHandle.WaitAll(doneEvents);

...

/// <summary>

/// Subset task method.

/// </summary>

/// <param name="state"></param>

private void SubsetRasterBand(object state)

// The state object must be cast to the correct type, because the

// signature of the WaitForTimerCallback delegate specifies type

// Object.

TaskInfo ti = (TaskInfo)state;

//Deserialize the workspace connection properties.

IXMLSerializer xmlSerializer = new XMLSerializerClass();

object obj =
xmlSerializer.LoadFromString(ti.InputRasterWSConnectionProps, null,

null);

IPropertySet workspaceProperties = (IPropertySet)obj;


...

Synchronizing execution of concurrent running threads


In many cases, you have to synchronize the execution of concurrently running threads. Normally,
you have to wait for one or more threads to finish their tasks, signal a waiting thread to resume its
task when a certain condition is met, test whether a given thread is alive and running, change a
thread priority, or give some other indication.

In .NET, there are several ways to manage the execution of running threads. The main classes
available to help thread management are as follows:

• System.Threading.Thread—Used to create and control threads, change priority, and get


status

• System.Threading.WaitHandle—Defines a signaling mechanism to indicate taking or


releasing exclusive access to a shared resource, allowing you to restrict access to a block of
code

• Calling the WaitHandle.WaitAll() method must be done from a multithreaded


apartment (MTA) thread. To launch multiple synchronized tasks, you first have to
launch a worker thread that, in turn, will run the multiple threads.

• System.Threading.Monitor—Similar to System.Threading.WaitHandle, provides a


mechanism that synchronizes access to objects

• System.Threading.AutoResetEvent and System.Threading.ManualResetEvent—


Used to notify waiting threads that an event has occurred, allowing threads to communicate
with each other by signaling

The following code example, also in the Multithreaded raster subset sample, extends what was
covered in the previous section. It uses the ManualResetEvent and the WaitHandle classes to wait
for multiple threads to finish their tasks. In addition, it demonstrates using the AutoResetEvent
class to block running threads from accessing a block of code and to signal the next available thread
when the current thread had completed its task.

[C#]

/// <summary>

/// Class used to pass the task information to the working thread.

/// </summary>

public class TaskInfo

//Signal the main thread that the thread has finished its task.

private ManualResetEvent m_doneEvent;

... public TaskInfo(int BandID, ManualResetEvent doneEvent)

m_bandID = BandID;
m_doneEvent = doneEvent;

... public ManualResetEvent DoneEvent

get

return m_doneEvent;

set

m_doneEvent = value;

//Block access to the shared resource (raster dataset).

private static AutoResetEvent m_autoEvent = new AutoResetEvent(false);

... public override void OnMouseDown(int Button, int Shift, int X, int Y)

...

// Run the subset thread that will spin off separate subset tasks. By
default, this thread will run as MTA.

// This is needed since to use WaitHandle.WaitAll(), the call must be


made

// from MTA.

/// <summary>

/// Main subset method.

/// </summary>

private void SubsetProc()


{

...

//Create ManualResetEvent to notify the main threads that

//all ThreadPool threads are done with their tasks.

ManualResetEvent[] doneEvents = new ManualResetEvent[m_intBandCount];

//Create a ThreadPool task for each band of the input raster.

//Each task will subset a different raster band.

//All information required for subsetting the raster band will be


passed to the ThreadPool

task by the user - defined class TaskInfo. for (int i = 0; i <


m_intBandCount; i

++)

//Create the ManualResetEvent field for the task to

// signal the waiting thread that the task had been completed.

doneEvents[i] = new ManualResetEvent(false);

TaskInfo ti = new TaskInfo(i, doneEvents[i]);

...

// assign the subsetting thread for the rasterband.

threadTask[i] = new Thread(new


ParameterizedThreadStart(SubsetRasterBand));

// Note the STA apartment which is required to run ArcObjects

threadTask[i].SetApartmentState(ApartmentState.STA);

threadTask[i].Name = "Subset_" + (i + 1).ToString();

// start the task and pass the task information

threadTask[i].Start((object)ti);

//Set the state of the event to signaled to allow one or more of the
waiting threads to proceed.

m_autoEvent.Set();
// Wait for all threads to complete their task…

WaitHandle.WaitAll(doneEvents);

...

/// <summary>

/// Subset task method.

/// </summary>

/// <param name="state"></param>

private void SubsetRasterBand(object state)

// The state object must be cast to the correct type, because the

// signature of the WaitOrTimerCallback delegate specifies type

// Object.

TaskInfo ti = (TaskInfo)state;

...

//Lock all other threads to get exclusive access.

m_autoEvent.WaitOne();

//Insert code containing your threading logic here.

//Signal the next available thread to get write access.

m_autoEvent.Set();

//Signal the main thread that the thread has finished its task.

ti.DoneEvent.Set();

Sharing a managed type across multiple threads


Sometimes your .NET application’s underlying data structure will be a managed object such as a
DataTable or HashTable. These .NET managed objects allow you to share them across multiple
threads such as a data fetching thread and a main rendering thread. However, you should
consult the MSDN Web site to verify whether an item is thread safe. In many cases, an object is
thread safe for reading but not for writing. Some collections implement a Synchronized method,
which provides a synchronized wrapper around the underlying collection.

In cases where your object is being accessed from more than one thread, you should acquire an
exclusive lock according to the Thread Safety section in MSDN regarding this particular object.
Acquiring such an exclusive lock can be done using one of the synchronization methods described in
the previous section or by using a lock statement, which marks a block as a critical section by
obtaining a mutual-exclusive lock for a given object. It ensures that if another thread attempts to
access the object, it will be blocked until the object is released (exits the lock).

The following screen shot demonstrates sharing a DataTable by multiple threads. First, check the
DataTable Class on the MSDN Web site to verify if it is thread safe.

On that page, check the section on Thread Safety, where it says, "This type is safe for
multithreaded read operations. You must synchronize any write operations."

This means that reading information out of the DataTable is not a problem, but you must prevent
other threads access to the table when you are about to write to it. The following code
example shows how to block those other threads:

[C#]

private DataTable m_locations = null;

...

DataRow rec = m_locations.NewRow();

rec["ZIPCODE"] = zipCode; //ZIP Code

rec["CITYNAME"] = cityName; //City name

//Lock the table and add the new record.

lock(m_locations)
{

m_locations.Rows.Add(rec);

Updating the UI from a background thread


In most cases where you are using a background thread to perform lengthy operations, you want to
report to the user the progress, status, errors, or other information related to the task performed by
the thread. This can be done by updating a control on the application's UI. However, in Windows,
forms controls are bound to a specific thread (generally the main thread) and are not thread safe.
As a result, you must delegate, and thus marshal, any call to a UI control to the thread to which the
control belongs. The delegation is done through calling the Control.Invoke method, which executes
the delegate on the thread that owns the control's underlying window handle. To verify whether a
caller must call an invoke method, you can use the property Control.InvokeRequired. You must
make sure that the control's handle was created before attempting to call Control.Invoke or
Control.InvokeRequired.

The following code example, an excerpt from the RSS weather layer sample, demonstrates
reporting a background task's progress into a user form.

1. In the user form, declare a delegate through which you will pass the information to the
control.

[C#]

public class WeatherItemSelectionDlg: System.Windows.Forms.Form

private delegate void AddListItmCallback(string item);

� � �

2. In the user form, set the method to update the UI control. Notice the call to Invoke. The
method must have the same signature as the previously declared delegate:

[C#]

//Make thread-safe calls to Windows Forms Controls.

private void AddListItemString(string item)

// InvokeRequired compares the thread ID of the

//calling thread to the thread ID of the creating thread.

// If these threads are different, it returns true.

if (this.lstWeatherItemNames.InvokeRequired)

//Call itself on the main thread.

AddListItmCallback d = new AddListItmCallback(AddListItemString);


this.Invoke(d, new object[]

item

);

else

//Guaranteed to run on the main UI thread.

this.lstWeatherItemNames.Items.Add(item);

3. On the background thread, implement the method that will use the delegate and pass over
the message to be displayed on the user form.

[C#]

private void PopulateSubListProc()

//Insert code containing your threading logic here.

//Add the item to the list box.

frm.AddListItemString(data needed to update the UI control, string in


this case )

4. Write the call that launches the background thread itself, passing in the method written in
Step 3.

[C#]

//Generate a thread to populate the list with items that match the
selection criteria.

Thread t = new Thread(new ThreadStart(PopulateSubListProc));

t.Start();

Calling ArcObjects from a thread other than the main thread


In many multithreading applications, you will need to make calls to ArcObjects from different
running threads. For example, you might have a background thread that gets a response from a
Web service, which, in turn, should add a new item to the map display, change the map extent, or
run a geoprocessing (GP) tool to perform some type of analysis.

A very common case is calling ArcObjects from a timer event handler method. Timer's Elapsed
event is raised on a ThreadPool task, a thread that is not the main thread. Yet it needs to
use ArcObjects, which looks like it would require cross-apartment calls. However, this can be
avoided by treating the ArcObjects component as if it were a UI control and using Invoke to
delegate the call to the main thread where the ArcObjects component is created. Thus, no cross-
apartment calls are made.

The ISynchronizeInvoke interface includes methods Invoke, BeginInvoke, and EndInvoke.


Implementing these methods yourself can be a daunting task. Instead, you should either have your
class directly inherit from System.Windows.Forms.Control or you should have a helper class that
inherits Control. Either option will provide a simple and efficient solution for invoking methods.

The following code example employs a user-defined InvokeHelper class to invoke a timer’s elapsed
event handler to recenter the map's visible bounds and set the map's rotation. Note that some of
the application logic must be done on the InvokeHelper class in addition to the user-defined
structure that is being passed by the method delegate.

[C#]

/// <summary>

/// A helper method used to delegate calls to the main thread.

/// </summary>

public sealed class InvokeHelper: Control

//Delegate used to pass the invoked method to the main thread.

public delegate void MessageHandler(NavigationData navigationData);

//Class members.

private IActiveView m_activeView;

private IPoint m_point = null;

/// <summary>

/// Class constructor.

/// </summary>

/// <param name="activeView"></param>

public InvokeHelper(IActiveView activeView)

//Make sure that the control was created and that it has a valid
handle.
this.CreateHandle();

this.CreateControl();

//Get the active view.

m_activeView = activeView;

/// <summary>

/// Delegate the required method onto the main thread.

/// </summary>

/// <param name="navigationData"></param>

public void InvokeMethod(NavigationData navigationData)

// Invoke HandleMessage through its delegate.

if (!this.IsDisposed && this.IsHandleCreated)

Invoke(new MessageHandler(CenterMap), new object[]

navigationData

);

/// <summary>

/// The method that gets executed by the delegate.

/// </summary>

/// <param name="navigationData"></param>

public void CenterMap(NavigationData navigationData)

//Get the current map visible extent.

IEnvelope envelope =

m_activeView.ScreenDisplay.DisplayTransformation.VisibleBounds;
if (null == m_point)

m_point = new PointClass();

//Set the new map center coordinate.

m_point.PutCoords(navigationData.X, navigationData.Y);

//Center the map around the new coordinate.

envelope.CenterAt(m_point);

m_activeView.ScreenDisplay.DisplayTransformation.VisibleBounds =
envelope;

//Rotate the map to the new rotation angle.

m_activeView.ScreenDisplay.DisplayTransformation.Rotation =

navigationData.Azimuth;

/// <summary>

/// Control initialization.

/// </summary>

private void InitializeComponent(){}

/// <summary>

/// A user defined data structure used to pass information to the


invoke method.

/// </summary>

public struct NavigationData

public double X;

public double Y;

public double Azimuth;


/// <summary>

/// Struct constructor.

/// </summary>

/// <param name="x">map x coordinate</param>

/// <param name="y">map x coordinate</param>

/// <param name="azimuth">the new map azimuth</param>

public NavigationData(double x, double y, double azimuth)

X = x;

Y = y;

Azimuth = azimuth;

/// <summary>

/// This command triggers the tracking functionality.

/// </summary>

public sealed class TrackObject: BaseCommand

//Class members.

private IHookHelper m_hookHelper = null;

� � � private InvokeHelper m_invokeHelper = null;

private System.Timers.Timer m_timer = null;

� � �

/// <summary>

/// Occurs when this command is created.

/// </summary>

/// <param name="hook">Instance of the application</param>

public override void OnCreate(object hook)

� � �
//Instantiate the timer.

m_timer = new System.Timers.Timer(60);

m_timer.Enabled = false;

//Set the timer's elapsed event handler.

m_timer.Elapsed += new ElapsedEventHandler(OnTimerElapsed);

/// <summary>

/// Occurs when this command is clicked.

/// </summary>

public override void OnClick()

//Create the InvokeHelper class.

if (null == m_invokeHelper)

m_invokeHelper = new
InvokeHelper(m_hookHelper.ActiveView);

...

//Start the timer.

if (!m_bIsRunning)

m_timer.Enabled = true;

else

m_timer.Enabled = false;

...

/// <summary>

/// Timer elapsed event handler.

/// </summary>

/// <param name="sender"></param>

/// <param name="e"></param>

private void OnTimerElapsed(object sender, ElapsedEventArgs e)

...
//Create the navigation data structure.

NavigationData navigationData = new


NavigationData(currentPoint.X,

currentPoint.Y, azimuth);

//Update the map extent and rotation.

m_invokeHelper.InvokeMethod(navigationData);

...

Multithreading with geoprocessing


To use GP in an asynchronous/multithreaded application, use the asynchronous execution pattern
exposed in ArcGIS Server 9.2 using GP Services. This pattern enables the desktop working with GP
in an asynchronous execution mode.

Working with OLE StdFont and StdPicture classes (ESRI.ArcGIS.ADF)

About working with OLE StdFont and StdPicture classes (ESRI.ArcGIS.ADF)


Some ArcObjects libraries make use of classes and interfaces defined within the standard Object
Linking and Embedding (OLE) libraries from Microsoft. To use these with .NET, you should add to
your project a reference to the Stdole.dll primary interop assembly (PIA), which is included as part
of the .NET support during an ArcGIS installation. This PIA allows you to define StdFont and
StdPicture classes. See the following code example:

[C#]

stdole.IFontDisp fnt = (stdole.IFontDisp)new stdole.StdFontClass();

fnt.Name = "Arial";

fnt.Size = 20.0F;

ESRI.ArcGIS.Display.TextSymbol textSym = new


ESRI.ArcGIS.Display.TextSymbolClass();

textSym.Font = fnt;

[VB.NET]

Dim fnt As stdole.IFontDisp = New stdole.StdFontClass()

fnt.Name = "Arial"

fnt.Size = 20.0

Dim textSym As ESRI.ArcGIS.Display.TextSymbol = New


ESRI.ArcGIS.Display.TextSymbolClass()

textSym.Font = fnt

Sometimes, however, you may have an existing .NET font, bitmap, or icon class that you want to
convert to use as a font or picture in an ESRI method. The ESRI.ArcGIS.ADF.COMSupport
namespace, which is part of the ESRI.ArcGIS.ADF.dll assembly, provides the OLE class that can help
you to perform such conversions.

These members depend on the System.Windows.Forms.AxHost class and as such are only suitable
for use within a project that has a reference to the System.Windows.Forms.dll assembly.

Syntax information
The following shows syntax information for the members of the
ESRI.ArcGIS.Utility.COMSupport.OLE class. These are static (shared in VB.NET) members and
therefore can be called without the need to instantiate the OLE class.

• GetIFontDispFromFont—This method can be used to convert an existing .NET


System.Drawing.Font object into a Stdole.StdFont object.

[C#]

public static object GetIFontDispFromFont(System.Drawing.Font font)

[VB.NET]

Public Shared GetIFontDispFromFont (ByVal font As System.Drawing.Font) As


Object

• GetIPictureDispFromBitmap—This method can be used to convert an existing .NET


System.Drawing.Bitmap object into a Stdole.StdPicture object.

[C#]

public static object GetIPictureDispFromBitmap(System.Drawing.Bitmap


bitmap)

[VB.NET]

Public Shared GetIPictureDispFromBitmap (ByVal bitmap As


System.Drawing.Bitmap) As Object

• GetIPictureDispFromIcon—This method can be used to convert an existing .NET


System.Drawing.Icon object into a Stdole.StdPicture object.

[C#]

public static object GetIPictureDispFromIcon(System.Drawing.Icon icon)

[VB.NET]

Public Shared GetIPictureDispFromIcon (ByVal icon As System.Drawing.Icon)


As Object

Using members of the OLE class


The following are some code examples of using the members of the OLE class:

[C#]
System.Drawing.Font dotNetFont = new System.Drawing.Font("Castellar",
25.0F);

ESRI.ArcGIS.Display.ITextSymbol textSym = new


ESRI.ArcGIS.Display.TextSymbolClass()

as ESRI.ArcGIS.Display.ITextSymbol;

textSym.Font =
ESRI.ArcGIS.ADF.COMSupport.OLE.GetIFontDispFromFont(dotNetFont)as

stdole.IFontDisp;

System.Drawing.Bitmap dotNetBmp = new


System.Drawing.Bitmap(@"C:\Temp\MyBitmap.bmp");

ESRI.ArcGIS.Display.IPictureMarkerSymbol bmpSym = new

ESRI.ArcGIS.Display.PictureMarkerSymbolClass()as

ESRI.ArcGIS.Display.IPictureMarkerSymbol;

bmpSym.Picture =
ESRI.ArcGIS.ADF.COMSupport.OLE.GetIPictureDispFromBitmap(dotNetBmp)

as stdole.IPictureDisp;

System.Drawing.Icon dotNetIcon = new


System.Drawing.Icon(@"C:\Temp\MyIcon.ico");

ESRI.ArcGIS.MapControl.IMapControlDefault map = this.axMapControl1.Object


as

ESRI.ArcGIS.MapControl.IMapControlDefault;

map.MouseIcon =
ESRI.ArcGIS.ADF.COMSupport.OLE.GetIPictureDispFromIcon(dotNetIcon)as

stdole.IPictureDisp;

map.MousePointer =
ESRI.ArcGIS.SystemUI.esriControlsMousePointer.esriPointerCustom;

[VB.NET]

Dim dotNetFont As New System.Drawing.Font("Castellar", 25.0F)

Dim textSym As ESRI.ArcGIS.Display.ITextSymbol = New


ESRI.ArcGIS.Display.TextSymbolClass

textSym.Font =
ESRI.ArcGIS.ADF.COMSupport.OLE.GetIFontDispFromFont(dotNetFont)

Dim dotNetBmp As System.Drawing.Bitmap = New


System.Drawing.Bitmap("C:\Temp\MyBitmap.bmp")
Dim bmpSym As ESRI.ArcGIS.Display.IPictureMarkerSymbol = New
ESRI.ArcGIS.Display.PictureMarkerSymbolClass

bmpSym.Picture =
ESRI.ArcGIS.ADF.COMSupport.OLE.GetIPictureDispFromBitmap(dotNetBmp)

Dim dotNetIcon As System.Drawing.Icon = New


System.Drawing.Icon("C:\Temp\MyIcon.ico")

Dim map As ESRI.ArcGIS.MapControl.IMapControlDefault =


Me.AxMapControl1.Object

map.MouseIcon =
ESRI.ArcGIS.ADF.COMSupport.OLE.GetIPictureDispFromIcon(dotNetIcon)

map.MousePointer =
ESRI.ArcGIS.SystemUI.esriControlsMousePointer.esriPointerCustom

How to use IGeometryBridge to update dynamic geometries

SummaryIGeometryBridge is a powerful interface that allows you to overcome most of the issues
for using ArcObjects geometries in a highly-dynamic environment and for using geometries across
multiple threads.

Development licensing Deployment licensing

Engine Developer Kit Engine Runtime

ArcView ArcView

ArcEditor ArcEditor

ArcInfo ArcInfo

About using IGeometry to update dynamic geometries


When it comes to implementing a dynamic geographic information system (GIS) in the .NET
environment—by design or due to the nature of the .NET platform—you may need to work in
multiple threads. You may be required to update geometries on different events, such as timer
events or other asynchronous callbacks. Also, you may have to share geometries across multiple
threads (for an instance in an architecture where there is one rendering thread and other update
threads).

Since ArcObjects geometries are Component Object Model (COM) components marked as a single
threaded apartment (STA), if you call these geometries from any non-main thread in your
application, you pay the penalty of cross apartment calls, which significantly affects the
performance of your application.

When it comes to managing points data, there is a straightforward solution—manage your point
as raw x,y coordinates, then update the point geometry on the main thread as required. However,
when it comes to geometries such as polylines and polygons, the problem becomes trickier to solve.
Before getting into the solution for these geometries, consider another factor that can affect the
performance of your .NET application—the overhead of the COM interop when there is extensive
allocation of new ArcObjects. For example, if you are creating new ArcObjects geometries or update
existing ones, there are a number of calls across the interop layer. In particular, if you are
creating ArcObjects in a tight loop, you repeatedly have the delay from crossing the interop layer,
which drastically slows your application. For more information on the issues interacting with the
COM interop, see Performance of ArcObjects.

To address the previous issues, IGeometryBridge comes in handy. It allows you to create and
update your ArcObjects geometries using simple types, which means that sharing these geometries
across multiple threads is no longer an issue, as well as reducing the issue of the interop overhead
to an absolute minimum.

This is where the WKSPoint structure becomes beneficial. WKSPoint is defined as a simple C++
structure, which means it is treated by the .NET framework as a simple type (using it across
multiple threads is permissible and there is almost no overhead for using it in a .NET environment).

By managing your geometries as arrays of WKSPoints, you can safely share them across multiple
threads and avoid the overhead of allocating COM objects. Since most ArcObjects analysis and
drawing methods require a standard ArcObjects geometry, the technique involves keeping a class
member of one geometry together with an instance of a geometry bridge (GeometryEnvironement).

In most cases, there are two scenarios where you may be required to update geometries:

• When the entire geometry constantly changes and you do not know which of the vertices
changed, as well as the overall number of vertices beforehand

• When only known vertices change on each update cycle

In the following sections, you will find step-by-step instructions for implementing these two
scenarios.

Update an entire geometry on each update cycle


This scenario is possible when you are receiving geometries from an external feed and have to
analyze or draw these geometries using ArcObjects. Any geometry is different from the other;
therefore, you do not know any information on the geometry in advance. In such cases, you have to
constantly update the entire ArcObjects geometry each time you get a new geometry from the
external feed.

1. In your class, add three additional class members to handle the geometry—a point
collection that stores the ArcObjects geometry, an empty one-dimensional array of
WKSPoints, and a geometry bridge. See the following code example:

[C#]

private IPointCollection4 m_polygon = null;

private WKSPoint[] m_wksPoints = null;

private IGeometryBridge2 m_geometryBridge = null;

[VB.NET]

Private m_polygon As IPointCollection4 = Nothing

Private m_wksPoints() As WKSPoint = Nothing


Private m_geomeTryBridge As IGeomeTryBridge2 = Nothing

2. Instantiate the ArcObjects geometry and the geometry bridge. Make sure the instantiation
is done on the application's main thread (must be defined as STA thread). See the following
code example:

[C#]

m_polygon = new PolygonClass();

// Create the geometry environment class instance.

m_geometryBridge = new GeometryEnvironmentClass();

[VB.NET]

m_polygon = New PolygonClass()

' Create the geometry environment class instance.

m_geomeTryBridge = New GeomeTryEnvironmentClass()

• To update the ArcObjects geometry, populate the WKSPoint array and use the geometry
bridge to replace all existing vertices of the ArcObjects geometry with the vertices for the
WKSPoint array.

3. Populate the WKSPoint array. You can update the WKSPoint array from anywhere in your
code. You can also do this on a different thread, as long as you synchronize the threads.
See the following code example:

[C#]

int count = 0;

double X;

double Y;

lock(m_wksPoints)

m_wksPoints = new WKSPoint[m_table.Rows.Count];

foreach (DataRow r in m_table.Rows)

//Get the item's coordinate.

X = Convert.ToDouble(r["X"]);
Y = Convert.ToDouble(r["Y");

// Update the geometry's array.

m_wksPoints[count].X = X;

m_wksPoints[count].Y = Y;

count++;

[VB.NET]

Dim Count As Integer = 0

Dim X As Double

Dim Y As Double

SyncLock m_wksPoints

m_wksPoints = New WKSPoint(m_table.Rows.Count - 1){}

For Each r As DataRow In m_table.Rows

' Get the item's coordinate.

X = Convert.ToDouble(r("X"))

Y = Convert.ToDouble(r("Y"))

' Update the geometry's array.

m_wksPoints(Count).X = X

m_wksPoints(Count).Y = Y

Count + = 1

Next r

End SyncLock

4. Update the ArcObjects geometry using the geometry bridge. Calling


IGeometryBridge2.SetWKSPoints replaces all existing vertices with the one specified by the
WKSPoint array. Again, if you are updating the WKSPoint array on a different thread, lock
the array before.

[C#]

// Update the ArcObjects geometry as needed.

lock(m_wksPoints)

m_geometryBridge.SetWKSPoints(m_polygon, ref m_wksPoints);

[VB.NET]

' Update the ArcObjects geometry as needed.

SyncLock m_wksPoints

m_geometryBridge.SetWKSPoints(m_polygon, m_wksPoints)

End SyncLock

5. When you are done updating the geometry, use it as needed. See the following code
example:

[C#]

DynamicDisplay.DrawPolygon(m_polygon);

[VB.NET]

DynamicDisplay.DrawPolygon(m_polygon)

Add a single vertex to an existing geometry on each update cycle


In other cases, you will only need to update a single vertex at a time, either adding an additional
vertex to your geometry (for an instance when drawing the trail of a moving object) or updating
vertices at a known index inside the geometry. In such cases, you only need to manage a single
WKSPoint and use IGeometryBridge2.InsertWKSPoints or IGeometryBridge2.AddWKSPoints. For
more information, see the sample Dynamic biking.

1. In your class, add three additional class members to handle the geometry. A point
collection that stores the ArcObjects geometry, a one-dimensional array of WKSPoints with
one element in it, and a geometry bridge. See the following code example:

[C#]

private IPointCollection4 m_bikeRouteGeometry = null;

private IGeometryBridge2 m_geometryBridge = null;

private WKSPoint[] m_wksPoints = new WKSPoint[1];


[VB.NET]

Private m_bikeRouteGeometry As IPointCollection4 = Nothing

Private m_geometryBridge As IGeometryBridge2 = Nothing

Private m_wksPoints As WKSPoint() = New WKSPoint(0){}

2. Instantiate the ArcObjects geometry and the geometry bridge. Make sure the instantiation
is done on the application's main thread (must be defined as STA thread). See the following
code example:

[C#]

m_bikeRouteGeometry = new PolylineClass();

// Create the geometry environment class instance.

m_geometryBridge = new GeometryEnvironmentClass();

[VB.NET]

m_bikeRouteGeometry = New PolylineClass()

' Create the geometry environment class instance.

m_geometryBridge = New GeometryEnvironmentClass()

3. Populate the WKSPoint inside the array (array with one element). You can update the
WKSPoint array from anywhere in your code. You can also do this on a different thread, as
long as you synchronize the threads. See the following code example:

[C#]

// Update the bike trail.

lock(m_wksPoints)

m_wksPoints[0].X = m_bikePositionInfo.position.X;

m_wksPoints[0].Y = m_bikePositionInfo.position.Y;

[VB.NET]

' Update the bike trail.

SyncLock m_wksPoints
m_wksPoints(0).X = m_bikePositionInfo.position.X

m_wksPoints(0).Y = m_bikePositionInfo.position.Y

End SyncLock

4. Update the ArcObjects geometry using the geometry bridge. Calling


IGeometryBridge2.AddWKSPoints adds the additional point at the end of the geometry.
Again, if you are updating the WKSPoint array on a different thread, lock the array before.
See the following code example:

[C#]

// Update the ArcObjects geometry.

lock(m_wksPoints)

m_geometryBridge.AddWKSPoints(m_bikeRouteGeometry, ref m_wksPoints);

[VB.NET]

' Update the ArcObjects geometry.

SyncLock m_wksPoints

m_geometryBridge.AddWKSPoints(m_bikeRouteGeometry, m_wksPoints)

End SyncLock

5. When you are done updating the geometry, use it as needed. See the following code
example:

[C#]

dynamicDisplay.DrawPolyline(m_bikeRouteGeometry);

[VB.NET]

dynamicDisplay.DrawPolyline(m_bikeRouteGeometry)

Hidden managed classes not appearing in VB.NET IntelliSense

About hidden managed classes not appearing in VB.NET IntelliSense


Hidden managed classes for the primary interop assemblies (PIAs) of ArcObjects 9.2 do not always
appear in Visual Basic (VB) .NET IntelliSense for Visual Studio 2005. These managed classes show
as hidden types in the Object Browser of Visual Studio.

All Component Object Model (COM) coclasses are converted to managed classes; the managed
classes have the same name as the original with "Class" appended. This conversion is part of ESRI's
provided PIAs for ArcObjects in .NET. For example, the runtime-callable wrapper for the Point
coclass is PointClass. The managed classes that are available are shown as hidden types in the
Visual Studio 2005 Object Browser. See the following screen shot:

On some computers, IntelliSense in Visual Studio 2005 for VB.NET will not display these hidden
types, so typing the following code in Visual Studio will not show the hidden types. See the following
screen shot:

On other computers, typing the following code in Visual Studio will show the hidden types. See the
following screen shot:
While IntelliSense may not display the hidden managed classes, you can use those classes in your
project and compile solutions.

The behavior of IntelliSense regarding hidden managed classes in C# of Visual Studio 2005 has not
been observed.

How to implement error handling

SummaryThe error handling construct in Visual Studio .NET is known as structured exception
handling. The constructs used may be new to Visual Basic users but should be familiar to users of
C++ or Java.

Exception handling
Structured exception handling implementation is straightforward, and the same concepts are
applicable to VB.NET or C#. VB.NET allows backward compatibility by also providing unstructured
exception handling via the familiar OnError GoTo statement and Err object, although this model is
not discussed in this topic.

Using exceptions
Exceptions are used to handle error conditions in Visual Studio .NET and provide information about
the error condition. An exception is an instance of a class that inherits from the System.Exception
base class. Many different types of exceptions are provided by the .NET Framework, and it is also
possible to create your own exceptions. Each type extends the basic functionality of the
System.Exception by allowing further access to information about the specific type of error that has
occurred. An instance of an exception is created and thrown when the .NET Framework encounters
an error condition. You can handle exceptions by using the Try, Catch, and Finally construct.

Try, Catch, and Finally construct


This construct allows you to catch errors that are thrown within your code. An example of this
construct is shown below. An attempt is made to rotate an envelope, which throws an error. See the
following code example:

[C#]

try
{

IEnvelope env = new EnvelopeClass();

env.PutCoords(0D, 0D, 10D, 10D);

ITransform2D trans = (ITransform2D)env;

trans.Rotate(env.LowerLeft, 1D);

catch (System.Exception ex)

MessageBox.Show("Error: " + ex.Message);

// Clean up the code.

[VB.NET]

Try

Dim env As IEnvelope = New EnvelopeClass()

env.PutCoords(0D, 0D, 10D, 10D)

Dim trans As ITransform2D = env

trans.Rotate(env.LowerLeft, 1D)

Catch ex As System.Exception

MessageBox.Show("Error: " + ex.Message)

' Clean up the code.

End Try

Place a Try block around code that can fail. If the application throws an error within the Try block,
the point of execution switches to the first Catch block. The Catch block handles a thrown error. The
application executes the Catch block when the Type of a thrown error matches the error Type
specified by the Catch block. You can have more than one Catch block to handle different kinds of
errors. The following code example verifies if the exception thrown is a DivideByZeroException:

[C#]
catch (DivideByZeroException divEx)

// Perform divide by zero error handling.

catch (System.Exception ex)

// Perform general error handling.

...

[VB.NET]

...

Catch divEx As DivideByZeroException

' Perform divide by zero error handling.

Catch ex As System.Exception

' Perform general error handling.

...

If you do have more than one Catch block, the more specific exception Types should precede the
general System.Exception, which will always succeed the type check.

The application always executes the Finally block after the Try block completes, or after a Catch
block if an error was thrown. Therefore, the Finally block should contain code that must always be
executed, for example, to clean up resources such as file handles or database connections.

If you do not have any cleanup code, you do not need to include a Finally block.

Code without exception handling


If a line of code that is not contained in a Try block throws an error, the .NET runtime searches for a
Catch block in the calling function, continuing up the call stack until a Catch block is found.

If a Catch block is not specified in the call stack, the exact outcome can depend on the location of
the executed code and the configuration of the .NET runtime. Therefore, it is advisable to at least
include a Try, Catch, Finally construct for all entry points to a program.

Errors from COM components


The structured exception handling model differs from the HRESULT model used by the component
object model (COM). C++ developers can easily ignore an error condition in an HRESULT.
However, in Visual Basic 6, an error condition in an HRESULT populates the Err object and raises an
error.

The .NET runtime handling of errors from COM components is similar to the way COM errors were
handled at Visual Basic 6. If a .NET program calls a function in a COM component (through the COM
interop services), and returns an error condition as the HRESULT, the HRESULT is used to populate
an instance of the COMException. This is then thrown by the .NET runtime, where it can be handled
in the usual way by using a Try, Catch, Finally block.

Therefore, it is advisable to enclose all code that can raise an error in a COM component within a
Try block, with a corresponding Catch block to catch a COMException. The following code example is
the first example rewritten to check for an error from a COM component:

[C#]

IEnvelope env = new EnvelopeClass();

env.PutCoords(0D, 0D, 10D, 10D);

ITransform2D trans = (ITransform2D)env;

trans.Rotate(env.LowerLeft, 1D);

catch (COMException COMex)

if (COMex.ErrorCode == - 2147220984)

MessageBox.Show("You cannot rotate an Envelope");

MessageBox.Show("Error " + COMex.ErrorCode.ToString() + ": " +


COMex.Message);

catch (System.Exception ex)

MessageBox.Show("Error: " + ex.Message);

...

[VB.NET]
Dim env As IEnvelope = New EnvelopeClass()

env.PutCoords(0D, 0D, 10D, 10D)

Dim trans As ITransform2D = env

trans.Rotate(env.LowerLeft, 1D)

Catch COMex As COMException

If (COMex.ErrorCode = -2147220984) Then

MessageBox.Show("You cannot rotate an Envelope")

MessageBox.Show _

("Error " + COMex.ErrorCode.ToString() + ": " + COMex.Message)

End If

Catch ex As System.Exception

MessageBox.Show("Error: " + ex.Message)

...

The COMException belongs to the System.Runtime.InteropServices namespace. It provides access


to the value of the original HRESULT via the ErrorCode property, which you can test to determine
the error condition that occurred.

Throwing errors and the exception hierarchy


If you are coding a user interface, you may want to correct the error condition in the code and try
the call again. Alternatively, you may want to report the error to users to let them decide the course
of action to take. You can make use of the message property of the exception to identify the
problem.

However, if you are writing a function that is only called from other code, you may want to deal with
an error by creating a specific error condition and propagating this error to the caller. You can do
this using the Throw keyword.

To throw the existing error to the caller function, write your error handler using the Throw keyword.
See the following code example:

[C#]

catch (System.Exception ex)

throw;

}
...

[VB.NET]

Catch ex As System.Exception

...

If you want to propagate a different or more specific error back to the caller, create a new instance
of an exception, populate it appropriately, and throw this exception back to the caller. The following
code example uses the ApplicationException constructor to set the message property:

[C#]

catch (System.Exception ex)

throw new ApplicationException("You had an error in your application",


ex);

...

[VB.NET]

Catch ex As System.Exception

Throw New ApplicationException _

("You had an error in your application")

...

However, if you do this, the original exception is lost. To allow complete error information to be
propagated, the exception includes the InnerException property. This property should be set to
equal the caught exception before the new exception is thrown. This creates an error hierarchy.
Again, the following code example uses the ApplicationException constructor to set the
InnerException and message properties:

[C#]

catch (System.Exception ex)

System.ApplicationException appEx = new ApplicationException(

"You had an error in your application", ex);


throw appEx;

...

[VB.NET]

Catch ex As System.Exception

Dim appEx As System.ApplicationException = _

New ApplicationException("You
had an error in your application", ex)

Throw appEx

...

In this way, the function that eventually deals with the error condition can access all the information
about the cause of the condition and its context. If you throw an error, the application executes the
current function's Finally clause before control is returned to the calling function.

Writing your error handler


The best approach to handling an error depends on what error is thrown and in what context. For
more information, see Best Practices for Handling Exceptions on the MSDN Web site.

ArcObjects error codes

SummaryThis topic gives an overview of HRESULTs as they relate to ArcObjects and helps you
decipher HRESULT error codes you receive while developing and programming. This topic also
shows what an HRESULT is and how to locate information on these error codes in the ArcGIS .NET
software development kits (SDKs). Detailed instructions for the number conversions that are often
necessary when working with HRESULTs is also provided.

HRESULT error codes


When programming with ArcObjects in .NET, you can make calls to Component Object Model (COM)
components and therefore, receive errors from COM components. All COM methods return an
unsigned integer (HRESULT).

HRESULT is essentially a structure that shows whether the method succeeded or failed and contains
additional detailed information about the outcome of the operation. HRESULT values are often used
to return error information that is not specifically considered error codes. The .NET runtime handling
of errors from COM components is similar to the way Visual Basic 6 handled COM errors. If a .NET
program calls a function in a COM component, and returns an error condition as the HRESULT, the
HRESULT is used to populate an instance of the COMException. This exception is thrown by the .NET
runtime, where it can be handled by using a Try, Catch, Finally block. For more information on
implementing Try, Catch, Finally blocks and the COMException, see How to implement error
handling.

Most HRESULT error constants for ArcObjects libraries are documented throughout the ArcGIS .NET
SDK help system (see ArcObjects HRESULT error codes in this topic), which is available as part of
your SDK installed help or in the Documentation Library on the ESRI Developer Network (EDN) Web
site.
HRESULTs that are not specific to ArcObjects can be found on the Microsoft Developer Network
(MSDN) Web site. For a list of common MSDN HRESULT values, see Common HRESULT Values on
MSDN.

Locating HRESULTs
HRESULTs are returned in an eight-digit hexadecimal form (80004005) or a 10-digit decimal form
(–2147467259). The majority of the ESRI ArcObject's error code enumerations are in the 10-digit
decimal format.

The following ArcObjects HRESULT error codes table lists all available error enumeration pages and
the range of HRESULT values each library contains. If a library is not listed, there are currently no
specific HRESULT error codes for that library. Instead, you receive a general COM error. If you have
received an eight-digit hexadecimal error code, convert the eight-digit hexadecimal to a 10-digit
decimal. This conversion is not difficult and can be accomplished with the Windows Calculator.

Some of the ArcObjects libraries utilize a three-digit value instead of a 10-digit decimal value. These
libraries are ESRI.ArcGIS.Geometry (esriGeometryError and esriSpatialReferenceError
enumerations) and ESRI.ArcGIS.Geodatabase (esriDataConverterError enumeration). For
information on how to convert a 10-digit decimal value to a three-digit enumeration value, see
Converting 10-digit decimal value to three-digit enumeration value in this topic.

ArcObjects HRESULT error codes


The following table shows HRESULT error codes for ArcObjects:

Enumeration Library Min value Max value

esriDataConverterError Geodatabase 513 532

esriGeometryError Geometry 513 602

esriSpatialReferenceError Geometry 514 524

cadastralError Cadastral –2147221247 –2147220732

dimError Carto –2147220991 –2147220989

annoError Carto –2147220991 –2147220985

esriRasterLayerError Carto –2147217152 –2147217152

esriImageServerError Carto –2147216896 –2147216895

esriNETCDFError DataSourcesNetCDF –2147217408 –2147217401

esriRasterLoaderError DataSourcesRaster –2147217407 –2147217408

esriRasterError DataSourcesRaster –2147217408 –2147217370

esriRasterEncoderError DataSourcesRasterUI –2147217408 –2147217403

esriRepresentationDrawingError Display –2147218431 –2147218425


esriEditorError Editor –2147220990 –2147220990

esriSpatialAnalystError GeoAnalyst –2147217408 –2147217248

esriXYEventError Geodatabase –2147220991 –2147220983

esriNetworkErrors Geodatabase –2147220991 –2147220879

fdoError Geodatabase –2147220991 –2147212030

esriTinError Geodatabase –2147219456 –2147219388

esriRepresentationError Geodatabase –2147218687 –2147218670

esriGeoDataServerError GeoDatabaseDistributed –2147208551 –2147208540

esriTerrainError GeoDatabaseExtensions –2147205120 –2147219396

esriGeocodingError Location –2147220991 –2147220969

esriRouteEventError Location –2147220991 –2147220956

esriUtilityNetworkErrors NetworkAnalysis –2147220911 –2147220225

esriExportErrorReturnCodes Output –2147220735 –2147220719

esriOutputErrorReturnCodes Output –2147220686 –2147220670

esriSchematicErrors Schematic –2147218943 –2147218923

esriCoreErrorReturnCodes esriSystem –2147221247 –2147221243

messageSupportError esriSystem –2147220991 –2147220967

tascTMSWorkspaceError TrackingAnalyst –2147220480 –2147220352

tascGSDCoreError TrackingAnalyst –2147219456 –2147219392

tascTemporalLayerError TrackingAnalyst –2147219199 –2147219135

Converting eight-digit hexadecimal value to 10-digit decimal value


Do the following to convert an eight-digit hexadecimal value to a 10-digit decimal value:

1. Open Windows Calculator. If you do not see the Hex, Dec, Oct, and Bin radio buttons, click
View and click Scientific.
2. Select the Hex and Dword radio buttons.
3. Ensure the Num Lock is on, and type or paste the eight-digit hexadecimal value in the text
field (exclude 0x if it precedes the eight-digit number). See the following screen shot:

4. Click the Not button.

5. Click the + button, click the number 1 button, then click the = button to add one. See the
following screen shot:

6. Click the Bin radio button.


7. Click the Dec radio button.

8. Click the +/- button. See the following screen shot:

Converting 10-digit decimal value to three-digit enumeration value


If you have obtained a 10-digit error from the Geometry (esriGeometry and esriSpatialReference)
or Geodatabase (esriDataConverter) libraries, do the following steps to obtain the three-digit value
from your 10-digit error value.

1. Access Microsoft's error code lookup utility (ErrLook.exe) with Microsoft Visual Studio by
doing one of the following:
a. In Visual Studio, click Tools, External Tools, and click Add. Click the Ellipsis button
and navigate to Program Files\Microsoft Visual Studio 8\Common7\Tools, then click
errlook.exe. In the Open File dialog box, click Open. In the External Tools dialog
box, add a title for the tool and click OK. The tool now resides under the Tools
menu in Visual Studio.

-or-
b. Open Windows Explorer. Navigate to Program Files\Microsoft Visual Studio
8\Common7\Tools to use errlook.exe from this location.

2. Locate the 10-digit decimal HRESULT you obtained in Step 8 of the preceding section (that
is, –2147220984).

3. Open the ErrLook.exe utility (or access it from the Visual Studio Command Prompt). Type
or paste the 10-digit decimal HRESULT referenced in Step 2 in the Value field, including the
minus (-) sign in. See the following screen shot:
Alternatively, open the Visual Studio Command Prompt, type errlook at the prompt, and
press Enter to access the Error Lookup utility. See the following screen shot:

4. Click Lookup in the Error Lookup dialog box to get the converted value and ignore error
messages that may be returned, such as No error found. The hexadecimal conversion of
your HRESULT appears in the Value field. See the following screen shot:
There is a system error message associated with this HRESULT error code. Although it may
be confusing at first, the same error code values can be used across libraries and between systems
(that is, Microsoft and ESRI). In this case, you do not want the system error message; instead, you
need the ESRI error message.

5. Open the Windows Calculator and select the Hex radio button. Copy and paste the last
three digits (208) from the hexadecimal code you received in the errlook.exe utility. See
the following screen shot:

If you do not see the Hex radio button in the Windows Calculator, click the View menu and click
Scientific (instead of Standard).

6. Select the Dec radio button to convert to the three-digit value for the error code. See the
following screen shot:
7. Use the three-digit code (520) to find the error on the library documentation page.

Converting error codes without the Error Lookup utility


If you do not have the Error Lookup utility, do the following steps to convert from the 10-digit code
to the eight-digit hexadecimal value, then convert to the three-digit decimal value:

1. Open the Windows Calculator and select the Dec radio button.

2. Paste the 10-digit code (–2147220984) into the calculator. See the following screen shot:
3. Click the +/- button to get the positive number.

4. Select the Bin radio button. See the following screen shot:

5. Subtract 1.
6. Click the Not button (verify the Dword radio button is selected).

7. Select the Hex radio button to get the hexadecimal form of the error code. See the
following screen shot:
8. With the Hex radio button selected, make note of the last three digits (208) in the number.

9. With the Hex and Dword radio buttons selected, clear the calculator and add the
three digits from Step 8.

10. Select the Dec radio button to get the three-digit (520) value you need to look up the error
code in the documentation. See the following screen shot:

Debugging crashes using the ESRI Symbol Server

SummaryAt ArcGIS 9.3, the ArcGIS Desktop applications will be able to generate user-mode error
reports when ArcGIS applications fail. Error reports contain all the important information about the
application that was running when it failed. ArcGIS Developers are able to open the dump file within
Visual Studio and go directly to the function call where the error occurred. The minimum
requirements for working with an error report are Microsoft Visual Studio 2005 and access to the
ESRI ArcGIS Symbol Server. At ArcGIS 9.3, these error reports are created for ArcGIS 9.3 Desktop
Applications and Extensions. This excludes ArcEngine.

Introduction

ESRI is dedicated to continually improving the quality of its suite of ArcGIS Products. ArcGIS
Desktop applications provide technologies that capture software crash data that developers can
utilize as an additional debugging tool. If the ArcGIS application crashes, the user is prompted to
send in the error report (via a web service) to ESRI. In the event that ArcMap crashes due to a
custom application command or extension, developers can leverage the information contained in the
ESRI error report.

In order to view and debug an error report, you must download the symbol files from Microsoft and
ESRI and have Visual Studio 2005 (minimum) installed. The following sections detail error reports,
symbol files, and information for opening and debugging the error report in Visual Studio.

Error Reports
Beginning with ArcGIS 9.3, the ArcGIS Desktop applications and extension will produce an error
report if the application fails. An error report contains important information about where the failure
occurred. By default, the error dialog that appears allows the user to send the error report to ESRI
via a web service. Regardless of whether the user decides to send the error report, the most recent
10 error reports are saved to the following local user directory: %APPDATA%\ESRI\ErrorReports
and have a ".dmp" file extension. When the number of error reports in the directory exceeds 10, the
oldest error reports are automatically deleted as new error reports are saved.

To view an error report, download the appropriate symbol files first, and then open the error report
file in Visual Studio as outlined in the following sections.

Symbol Files

To debug an ArcGIS Desktop error report, you must have access to the Symbol files for both ArcGIS
and Microsoft. Symbol files provide a footprint of the functions that are contained in executable files
and dynamic-link libraries (DLLs). Additionally, symbol files can present a roadmap of the function
calls that lead to the point of failure. See the Microsoft article on How to use a Symbol server for
more information.

1. Launch Microsoft Visual Studio 2005 or 2008.


2. On the Tools menu, click Options.
3. In the Options dialog box, open the Debugging node, and click Symbols.
4. Edit the Symbol file (.pdb) locations and add http://msdl.microsoft.com/downloads/symbols
for Microsoft symbols and http://downloads2.esri.com/Support/symbols/ for ESRI symbols
(As shown below)

These servers are for downloading symbols only; they are not browseable.

5. Ensure ‘Search the above locations only when symbols are loaded manually’ is not checked.
6. Since you are using symbols on a remote symbol server, you can improve performance by
creating and specifying a local directory to which the symbols can be copied. To do this,
enter a path in the Cache symbols from symbol server to this directory box. (For example:
C:\SymbolsCache)
7. Click OK.
8. Since you are using the Microsoft public symbol server, you may encounter an End User
License Agreement dialog box as you initially access the symbols. Click ‘Yes’ to accept the
agreement and download symbols to your local cache.

The initial downloading of symbols from the symbol file location may take a substantial amount of
time.
Opening an Error Report and Finding the Exception
Although Visual Studio can read error reports that contain information about both managed code
and unmanaged code, for managed code you must use a tool called SOS from Microsoft. Please see
the following article for more information on downloading and using SOS:
http://msdn2.microsoft.com/en-us/library/yy6d2sxs.aspx

1. If it is not already open, launch Visual Studio 2005 or 2008.


2. On the File menu, click Open, and then click Project.
3. In the Open Project dialog box, locate and select the dump file. It will usually have a .dmp
extension.
4. Click OK.
5. Start debugging by pressing F5. This will take you to the location where the exception
occurred.

For more information on opening an error report, see Microsoft’s article on How to Save and Open
Dump Files.

For additional information on registry settings, see Error Report Registry Settings.

Error Report Registry Settings

ESRI recommends that developers only modify these settings when in the midst of internal
development of their own customization prior to deployment to end-users. Under no circumstances
should developers have users of their deployed customization redirect the error reporting
mechanism so that crash information is no longer sent to ESRI.
Registry Settings
ArcGIS administrators and developers can configure how the error reports operate for their
organization by customizing the local machine registry. By default, EnableErrorReport,
ShowErrorDialog, and EnableWebService are set to True.

Use the registry keys in the following table to customize the error reporting. In your computer’s
registry, navigate to HKCU\Software\ESRI\Settings\ErrorReports and create keys as necessary.

Warning: Serious problems might occur if you modify the registry incorrectly by using Registry
Editor or by using another method. These problems might require that you reinstall the operating
system. Before you edit the registry, export the keys in the registry that you plan to edit, or back
up the whole registry. If a problem occurs, you can then follow the steps in the "Restore the
registry" section to restore the registry to its previous state.

Registry Key Value Action


Name

EnableErrorReport DWORD (0 If set to 0, error reporting is disabled. You will see a dialog letting
or 1) you know that an error has occurred. An error report (.dmp) file is
saved to disk.

ShowErrorDialog DWORD (0 If set to 0, no dialog will be shown. The error report is saved to
or 1) your local disk only (not sent to ESRI).

EmailAddress String The email address that appears in the dialog when
EnableWebService is set to False (default
ArcGISErrorReport@esri.com). The error report is saved to your
local disk.

YourEmailAddress String The email address that will be sent to the web service. The error
report is saved to your local disk.

EnableWebService DWORD (0 If set to 1 send the error reports to the web service. The error
or 1) report is saved to your local disk.

CacheSize DWORD (0 The number of error reports to save on your disk. The error report
to 100) is saved to your local disk.

How to wire ArcObjects .NET events

SummaryAn event is the way a Windows application receives notification. In a Windows application,
many events are occurring at a particular instant—for example, Mouse Move, Mouse Out, and Mouse
Click. In .NET, you must hook events through delegates, which are function pointers (hold
references to functions). This document explains how to wire ArcObjects .NET events.

Working with events


An event is a message sent by an object to signal the occurrence of an action. The action can be
caused by user interaction, such as a mouse click, or it can be triggered by some other program
logic. The object that raises (triggers) the event is the event sender. The object that captures the
event and responds to it is the event receiver.

In event communication, the event sender class does not know which object or method receives
(handles) the event it raises. An intermediary or pointer mechanism is necessary between the
source and the receiver. The .NET Framework defines a special type (delegate) that provides the
functionality of a function pointer.

Delegates
A delegate is a class that holds a reference to a method. Unlike other classes, a delegate class has a
signature and can hold references only to methods that match its signature. A delegate is
equivalent to a type-safe function pointer or a callback.

To consume an event in an application, you must provide an event handler (an event handling
method) that executes program logic in response to the event and register the event handler with
the event source. The event handler must have the same signature as the event delegate. This
process is event wiring.

When you create an instance of a delegate, you pass in the function name (as a parameter for the
delegate's constructor) that this delegate references. See the following code example:

[C#]

delegate int SomeDelegate(string s, bool b); //A delegate declaration.

[VB.NET]

Delegate Function SomeDelegate(ByVal s As String, ByVal b As Boolean) As


Integer 'A delegate declaration.

Saying that this delegate has a signature means it returns an int type and takes two parameters:
string and bool. For that reason, when you are about to hook an event (for example,
IGlobeDisplayEvents.AfterDraw), the event handler method must match the delegate
signature declared by the event interface delegate. To consume an event in an application, you
must provide an event handler (an event handling method) that executes program logic in response
to the event and register the event handler with the event source. This process is event wiring.

In ArcObjects.NET, event interfaces (also known as outbound interfaces) have an _Event suffix,
because they are automatically suffixed with _Event by the type library importer.

Required steps to start listening to an ArcObjects event

1. Cast the relevant event interface (you can also do this in line). See the following code
example:

[C#]

IGlobeDisplayEvents_Event globeDisplayEvents = (IGlobeDisplayEvents_Event)

m_globeDisplay;

[VB.NET]

Dim globeDisplayEvents As IGlobeDisplayEvents_Event = CType(m_globeDisplay,


IGlobeDisplayEvents_Event)
2. Register the event handler method. See the following code example:

[C#]

globeDisplayEvents.AfterDraw += new
IGlobeDisplayEvents_AfterDrawEventHandler

(OnAfterDraw);

[VB.NET]

AddHandler globeDisplayEvents.AfterDraw, AddressOf OnAfterDraw

3. Implement the event handler method specified by the signature of the delegate. See the
following code example:

[C#]

private void OnAfterDraw(ISceneViewer pViewer)

//Your event handler logic.

[VB.NET]

Private Sub OnAfterDraw(ByVal pViewer As ISceneViewer)

'Your event handler logic.

End Sub

4. Unwire the event from your application once you no longer need to listen to it. See the
following code example:

[C#]

((IGlobeDisplayEvents_Event)m_globeDisplay).AfterDraw -= new

IGlobeDisplayEvents_AfterDrawEventHandler(OnAfterDraw);

[VB.NET]

RemoveHandler (CType(m_globeDisplay, IGlobeDisplayEvents_Event)).AfterDraw,


AddressOf OnAfterDraw

Alternatively, let Visual Studio do the work so you don't have to add the delegate signature:

1. Cast the relevant event interface.


2. Start the registration of the requested event. Use the Visual Studio integrated development
environment (IDE) Intellisense capabilities to do most of the work for you. Once you add
the += operator, Visual Studio allows you to add the delegate by pressing the Tab
key. Visual Studio automatically adds the event handler method. Press the Tab key twice,
and Visual Studio registers the event handler and adds the event handler method. See the
following screen shot:

Before pressing the Tab key the second time, you can modify the default event handler method
name by using the arrow keys to scroll and change the name. Do notuse the mouse to accomplish
this.

Visual Studio adds the event handler method and automatically shows the event handler
implementation code.

Wiring events of member objects


In many cases, you will need to wire events of objects that are members of container objects and
are accessed through a property. For example, it is possible that you will need to implement a
command for a GlobeControl application and listen to one of the GlobeDisplayEvents (for
example, AfterDraw). Whether or not you use the IDE integration or write the command yourself, it
is almost the obvious choice to use GlobeHookHelper class, which allows your command to work in
ArcGlobe as well. In this case, the GlobeHookHelper is a class member. See the following code
example:

[C#]

//Class members.

private IGlobeHookHelper m_globeHookHelper = null;

public override void OnCreate(object hook)

//Initialize the hook helper.

if (m_globeHookHelper == null)

m_globeHookHelper = new GlobeHookHelper();

//Set the hook.

m_globeHookHelper.Hook = hook;

((IGlobeDisplayEvents_Event)m_globeHookHelper.GlobeDisplay).AfterDraw
+= new
IGlobeDisplayEvents_AfterDrawEventHandler(OnAfterDraw);

� � �

[VB.NET]

'Class members.

Private m_globeHookHelper As IGlobeHookHelper = Nothing

Public Overrides Sub OnCreate(ByVal hook As Object)

'Initialize the hook helper.

If m_globeHookHelper Is Nothing Then

m_globeHookHelper = New GlobeHookHelper()

End If

'Set the hook.

m_globeHookHelper.Hook = hook

AddHandler (CType(m_globeHookHelper.GlobeDisplay,
IGlobeDisplayEvents_Events)).AfterDraw, AddressOf OnAfterDraw

...

End Sub

Wiring of the AfterDraw event is done against the GlobeHookHelper.GlobeDisplay property.


When you run the previous code, after awhile, the event stops and fires without any exception or
warning. This is because internally, when you use the GlobeDisplay property of the
GlobeHookHelper to wire the event, the hook helper gets a reference to the actual GlobeDisplay and
calls AddRef() before passing it back to the client. The .NET Framework creates a local variable for
the returning object and wires the event. However, once method OnCreate is ended, the local
IGlobeDisplay variable that was created by the .NET Framework gets out of scope and is collected
by the garbage collector at some point (meaning that it will eventually call the Release() method).
Once this happens, events stop firing.

The correct way to program this situation is to keep a class member referencing the contained
object and thus prevent the garbage collector from disposing of it. This way, you are guaranteed
the event you are listening to continues to fire throughout the lifetime of your class (do not forget to
unwire the event once you are done listening).

Eventually, your code looks something like the following example:

[C#]

//Class members.
private IGlobeHookHelper m_globeHookHelper = null;

private IGlobeDisplay m_globeDisplay = null;

public override void OnCreate(object hook)

//Initialize the hook helper.

if (m_globeHookHelper == null)

m_globeHookHelper = new GlobeHookHelper();

//Set the hook.

m_globeHookHelper.Hook = hook;

//Get the GlobeDisplay from the hook helper.

m_globeDisplay = m_globeHookHelper.GlobeDisplay;

((IGlobeDisplayEvents_Event)m_globeDisplay).AfterDraw += new

IGlobeDisplayEvents_AfterDrawEventHandler(OnAfterDraw);

� � �

[VB.NET]

'Class members.

Private m_globeHookHelper As IGlobeHookHelper = Nothing

Private m_globeDisplay As IGlobeDisplay = Nothing

Public Overrides Sub OnCreate(ByVal hook As Object)

'Initialize the hook helper.

If m_globeHookHelper Is Nothing Then

m_globeHookHelper = New GlobeHookHelper()

End If

'Set the hook.


m_globeHookHelper.Hook = hook

'Get the GlobeDisplay from the hook helper.

m_globeDisplay = m_globeHookHelper.GlobeDisplay

AddHandler (CType(m_globeDisplay, IGlobeDisplayEvents_Events)).AfterDraw,


AddressOf OnAfterDraw

...

End Sub

How to listen to document events

SummaryArcGIS Desktop applications are all document based. Document events are fired when a
document is created, opened, or closed. This article shows you how to hook up these events in your
custom .NET components.

Get a reference to the document object

1. To listen to document events of an ArcGIS Desktop application, get a reference to the


document object from the application. (Code)

The following table shows example entry points to get a reference to the application object:

Implementation Method and parameter

Command ICommand.OnCreate() hook parameter

Extension IExtension.Startup() initializationData parameter

2. Once you get a reference to the document, cast it to its event interface. See the following
table:

Application Default event interface

ArcMap ESRI.ArcGIS.ArcMapUI.MxDocument

ArcScene ESRI.ArcGIS.ArcScene.SxDocument

ArcGlobe ESRI.ArcGIS.ArcGlobe.GMxDocument
3. Although the default event interface for each application is different, declare the type of the
variable as the common IDocumentEvents_Event interface. Add a reference to the
ESRI.ArcGIS.ArcMapUI assembly to your project and declare a modular variable,
m_docEvents, to hold on to the document object. (Code)

You can also reference the document by the event interfaces in the following table:

Application Namespace Event interface

ArcMap ESRI.ArcGIS.ArcMapUI IDocumentEvents_Event

ArcScene ESRI.ArcGIS.ArcScene ISxDocumentEvents_Event

ArcGlobe ESRI.ArcGIS.ArcGlobe IGMxDocumentEvents_Event

Wire the document event in the event interface


When wiring events, use the += operator in C# and the AddHandler keyword in Visual Basic (VB)
.NET. The NewDocument event is wired to listen to document creation. (Code)

Register handler method to the event

1. Next, define a method to handle the event. This handler method is called when the event is
fired. The handler method signature must match the event you are listening to. For the
NewDocument event, the handler method signature is simple. (Code)

• C#—The Visual Studio 2005 autocomplete feature for C# is very helpful. Once you
type the += operator, an autocomplete ToolTip is shown and code is automatically
stubbed out when the Tab key is pressed twice. (Code)

• VB .NET—You have to write the handler method and use the AddressOf keyword to
reference the method to complete the AddHandler statement. (Code)

2. Add code in the event handler method to complete this task.

Complete code
See the following code example of the complete code:

[C#]

using ESRI.ArcGIS.Framework;

using ESRI.ArcGIS.ArcMapUI;

namespace ArcGISProject

public class howToClass


{

//Event member variable.

private IDocumentEvents_Event m_docEvents = null;

//Wiring.

private void SetUpDocumentEvent(IDocument myDocument)

m_docEvents = myDocument as IDocumentEvents_Event;

m_docEvents.NewDocument += new
IDocumentEvents_NewDocumentEventHandler

(howToClass_NewDocument);

//Event handler method.

void howToClass_NewDocument()

System.Windows.MessageBox.Show("New document at: " +

DateTime.Now.ToLongTimeString());

[VB.NET]

Imports ESRI.ArcGIS.Framework

Imports ESRI.ArcGIS.ArcMapUI

Public Class howToClass

'Event member variable.

Private m_docEvents As IDocumentEvents_Event


'Wiring.

Sub SetUpDocumentEvent(myDocument As IDocument)

m_docEvents = CType(myDocument, IDocumentEvents_Event)

AddHandler m_docEvents.NewDocument, AddressOf OnNewDocument

End Sub

'Event handler method.

Sub OnNewDocument()

MsgBox("New document at: " + DateTime.Now.ToLongTimeString())

End Sub

End Class

Code fragments

• Code 1 : Getting document object from application—This code requires referencing


ESRI.ArcGIS.Framework assembly in the project. See the following:

[C#]

using ESRI.ArcGIS.Framework;

� � � IApplication app = hook as IApplication;

IDocument appDocument = app.Document;

[VB.NET]

Imports ESRI.ArcGIS.Framework

Dim app As IApplication = CType(hook, IApplication)

Dim appDocument As IDocument = app.Document

• Code 2 : Declare an IDocumentEvents_Event variable—This code requires


referencing ESRI.ArcGIS.Framework and ESRI.ArcGIS.ArcMapUI in the project. See the
following:

[C#]
using ESRI.ArcGIS.Framework;

using ESRI.ArcGIS.ArcMapUI;

public class howToClass

private IDocumentEvents_Event m_docEvents = null;

private void SetUpDocumentEvent(IDocument myDocument)

m_docEvents = myDocument as IDocumentEvents_Event;

� � �

[VB.NET]

Imports ESRI.ArcGIS.Framework

Imports ESRI.ArcGIS.ArcMapUI

Public Class howToClass

Private m_docEvents As IDocumentEvents_Event

Sub SetUpDocumentEvent(myDocument As IDocument)

m_docEvents = CType(myDocument, IDocumentEvents_Event)

End Sub

End Class

• Code 3 : Wiring NewDocument event—This code requires


referencing ESRI.ArcGIS.Framework and ESRI.ArcGIS.ArcMapUI in the project. See the
following:

[C#]

private IDocumentEvents_Event m_docEvents = null;


private void SetUpDocumentEvent(IDocument myDocument)

m_docEvents = myDocument as IDocumentEvents_Event;

m_docEvents.NewDocument += � � �

[VB.NET]

Private m_docEvents As IDocumentEvents_Event

Sub SetUpDocumentEvent(myDocument As IDocument)

m_docEvents = CType(myDocument, IDocumentEvents_Event)

AddHandler m_docEvents.NewDocument, ...

End Sub

• Code 4 : NewDocument event signature—See the following:

[C#]

void OnNewDocument()

[VB.NET]

Sub OnNewDocument()

• Code 5 : Wiring NewDocument event to the handling method (C#)—This code


requires referencing ESRI.ArcGIS.Framework and ESRI.ArcGIS.ArcMapUI in the project. See
the following:

[C#]

m_docEvents.NewDocument += new IDocumentEvents_NewDocumentEventHandler

(howToClass_NewDocument);

� � � void howToClass_NewDocument(){}

• Code 6 : Wiring NewDocument event to the handling method (VB .NET)—This code
requires referencing ESRI.ArcGIS.Framework and ESRI.ArcGIS.ArcMapUI in the project. See
the following:

[VB.NET]

AddHandler m_docEvents.NewDocument, AddressOf OnNewDocument


Sub OnNewDocument()

End Sub

Wiring custom events using IActiveViewEvents

SummaryWiring custom events requires multiple steps that can be confusing. There are several
terms that are new to .NET, and the flow of using custom events can be difficult to follow when
embedded in an application. This concept topic pulls from various other sources to more fully
explain and tie together wiring events in Visual Basic .NET (VB.NET) using the IActiveViewEvents
interface to demonstrate how it works.

Development licensing Deployment licensing

ArcView ArcView

ArcEditor ArcEditor

ArcInfo ArcInfo

Engine Developer Kit Engine Runtime

In this topic

• Flow of working with events

• Step 1: Declare a delegate object variable that handles the specific event

• Step 2: Create an instance of the delegate using the AddressOf operator

• Step 3: Dynamically associate an event handler to the delegate object

• Step 4: Dynamically remove an event handler

It is recommended that you first familiarize yourself with the following primer documents to get a
general understanding of working with events:

• How to wire ArcObjects .NET events

• Delegates and the AddressOf Operator

• AddressOf Operator

• Events and Event Handlers

• Interoperating with COM


Flow of working with events
The following code mosaic screen shot shows the flow of working with events:

The four steps to wiring and using an event, shown in the previous mosaic screen shot are
described in the following sections:

Step 1: Declare a delegate object variable that will handle the specific event
A member variable ( m_ActiveViewEventsAfterDraw ) is declared that is global in scope for the Class
Form1. This member variable is the delegate object that will later be used to hold a type-safe
function pointer for the AddHandler and RemoveHandler statements.

For this step, use the following specific line of code:

[VB.NET]

Private m_ActiveViewEventsAfterDraw As
ESRI.ArcGIS.Carto.IActiveViewEvents_AfterDrawEventHandler

In VB.NET, the various IActiveViewEvents handlers are not displayed in IntelliSense in Visual Studio
2005; they are considered hidden types and do not display as a drop-down option when typing the
dot (.) while coding. You can, however, see the names of the hidden types in the Visual Studio 2005
Object Browser and in the ArcObjects help system. See the following screen shot:
Step 2: Create an instance of the delegate using the AddressOf operator
The delegate object member variable (m_ActiveViewEventsAfterDraw) is set to an instance of the
IActiveViewEvents_AfterDrawEventHandler interface. This is a type-safe pointer to the procedure
(that is, a sub or function) to accomplish the work of your event. The sub,
OnActiveViewEventsAfterDraw, is the procedure that does the work whenever this event is fired.

For this step, use the following specific line of code:

[VB.NET]

m_ActiveViewEventsAfterDraw = New
ESRI.ArcGIS.Carto.IActiveViewEvents_AfterDrawEventHandler(AddressOf
OnActiveViewEventsAfterDraw)

Step 3: Dynamically associate an event handler to the delegate object


Using the AddHandler statement, you specify which ArcObjects event uses your delegate. In the
code mosaic screen shot shown previously, the delegate object member variable
( m_ActiveViewEventsAfterDraw ) is used for the .AfterDraw event of the IActiveViewEvents_Event
interface.

In ArcObjects for .NET, the event interfaces all have an _Event suffix. These event interfaces (also
known as outbound interfaces) are automatically suffixed with _Event by the type library importer.
When programming with ArcObjects .NET, use the event interfaces with the appended _Event
suffix as shown in the Visual Studio 2005 Object Browser. Again, in VB.NET, the various _Event
handlers are not displayed in IntelliSense in Visual Studio 2005; they are considered hidden types
and do not display as a drop-down option when typing the dot (.) while coding. See the following
screen shot:
Step 4: Dynamically remove an event handler
Using the RemoveHandler statement, you specify which ArcObjects event you want to remove from
a particular delegate. In the code mosaic screen shot shown previously, the delegate object
member variable (m_ActiveViewEventsAfterDraw) is removed as the delegate event of the
.AfterDraw event of the IActiveViewEvents_Event interface.

For this step, use the following specific line of code:

[VB.NET]

RemoveHandler CType(map,
ESRI.ArcGIS.Carto.IActiveViewEvents_Event).AfterDraw,
m_ActiveViewEventsAfterDraw

Dynamically adding and removing event handlers in ArcObjects for .NET allows for powerful and
flexible applications. Depending on your application, you can code one type of behavior (for
example, inserting balloon text boxes with a mouse click) in one part of your application and a
different behavior in another part of your application (for example, performing hyperlinks to other
documents with a mouse click) by dynamically wiring events to custom procedures. Delegates are
the mechanism for which a type-safe pointer can be utilized to use the AddHandler and
RemoveHandler statements.

How to listen to versioned events

SummaryThe IVersionEvents and the IVersionEvents2 interfaces expose several events that event
handlers can listen to, allowing custom applications and extensions to respond to versioning events
such as reconciliation and posting. This article explains how to create event listeners and handlers
for these events in C# and VB.NET.

Development licensing Deployment licensing

ArcView ArcView

ArcEditor ArcEditor

ArcInfo ArcInfo

Engine Developer Kit Engine Runtime

To use the code in this article, the following namespaces must be referenced via the using (C#) or
Imports (VB.NET) statements. It is also necessary to add the corresponding references to the
project in order to gain access to these APIs.

• ESRI.ArcGIS.Geodatabase

• ESRI.ArcGIS.esriSystem

Versioned events overview


Listeners can subscribe to events from the event interfaces of a version, IVersionEvents_Event and
IVersionEvents2_Event. These interfaces can be accessed by casting an IVersion reference.

IVersionEvents_Event publishes the following events:

• OnConflictsDetected

• OnReconcile

• OnRedefineVersion

• OnRefreshVersion

IVersionEvents2_Event publishes these additional events:

• OnArchiveUpdated

• OnBeginReconcile

• OnDeleteVersion

• OnPost
IVersionEvents2_Event does not extend IVersionEvents_Event.
A common practice for implementing versioned event handlers is to create a class dedicated to
listening to events, an event listener.

Creating an event handler


An event handler is a method within the event listener that defines the application's behavior in
response to an event. Event handlers must match the return type and parameter types of the
published events, but can be given any name. The following code shows an event handler for the
OnReconcile event without any behavior:

[C#]

public void OnReconcile(String targetVersionName, Boolean hasConflicts)

// TODO: Implement the handler.

[VB.NET]

Public Sub OnReconcile(ByVal targetVersionName As String, ByVal


hasConflicts As Boolean)

' TODO: Implement the event handling.

End Sub

Adding handlers to events


Adding a handler to an event varies slightly between C# and VB.NET.

In C# an instance of the event's delegate type must be created to encapsulate the event handler.
The delegate is instantiated much like a class, by using the new keyword. In VB.NET, the AddressOf
operator is used to create a procedure delegate for the method.

The delegate instance can then be added to the appropriate event on the event interface. In C#,
the += operator is used for this, while in VB.NET the AddHandler statement is used.

The following code example is an event listener's constructor, which instantiates a delegate and
adds it to the IVersionEvents.OnReconcile event. To tightly couple an event listener with a version,
the event listener's constructor can accept an IVersion parameter, then cast it to the event
interface.

[C#]

public EventListener(IVersion version)

// Cast the version to the event interface.

IVersionEvents_Event versionEvent = (IVersionEvents_Event)version;


// Instantiate the delegate type and add it to the event.

versionEvent.OnReconcile += new IVersionEvents_OnReconcileEventHandler

(OnReconcile);

[VB.NET]

Public Sub New(ByVal Version As IVersion)

' Cast the version to the event interface.

Dim versionEvent As IVersionEvents_Event = CType(Version,


IVersionEvents_Event)

' Create a procedure delegate and add it to the event.

AddHandler versionEvent.OnReconcile, AddressOf OnReconcile

End Sub

Instantiating a listener
Once the listener is completed, it can be used within custom applications. Instantiating a listener is
simple, as shown below:

[C#]

// Get the version to be listened to.

IVersion version = versionedWorkspace.FindVersion("QA");

// Create a listener.

EventListener eventListener = new EventListener(version);

[VB.NET]

' Get the version to be listened to.

Dim Version As IVersion = versionedWorkspace.FindVersion("QA")

' Create a listener.

Dim eventListener As EventListener = New EventListener(Version)

Complete listener implementation


The following shows the full implementation of the event listener described above:

[C#]
public class EventListener

public EventListener(IVersion version)

// Cast the version to the event interface.

IVersionEvents_Event versionEvent = (IVersionEvents_Event)version;

// Instantiate the delegate type and add it to the event.

versionEvent.OnReconcile += new
IVersionEvents_OnReconcileEventHandler

(OnReconcile);

public void OnReconcile(String targetVersionName, Boolean hasConflicts)

// TODO: Implement the event handling.

[VB.NET]

Public Class EventListener

Public Sub New(ByVal Version As IVersion)

' Cast the version to the event interface.

Dim versionEvent As IVersionEvents_Event = CType(Version,


IVersionEvents_Event)

' Create a procedure delegate and add it to the event.

AddHandler versionEvent.OnReconcile, AddressOf OnReconcile

End Sub

Public Sub OnReconcile(ByVal targetVersionName As String, ByVal


hasConflicts As Boolean)
' TODO: Implement the event handling.

End Sub

End Class

Wiring events in ArcGIS Desktop using IActiveViewEvents

PurposeThe purpose of this sample is to give the ArcGIS Desktop developer the experience of using
an ArcGIS Base Command template to launch a Windows form and show how to wire up events for
the IActiveViewEvents interface. Developers can learn how the process works for building the basic
infrastructure for user interaction with an ArcGIS Desktop client.
See Using the samples for help on compiling, setting up the debugger, and running the sample
(either an exe or dll).

1. Open the solution (.sln) file.


2. Ensure that you have <your ArcGIS install location>\Program
Files\ArcMap\bin\ArcMap.exe set as your debugger in Visual Studio.
3. When the application starts, open any existing ArcGIS map document (.mxd).
4. Choose the Tools and Customize menus in ArcMap to open the Customize dialog
box.
5. Click the Commands tab of the Customize dialog box and select Samples ArcGIS in
the Categories list box.
6. Drag the Wiring Events icon from the Commands list onto the graphical user
interface (GUI) to test the sample.
7. Close the Customize dialog box.
8. Click the newly added button to launch the Windows form demonstrating how the
wiring of events works.
9. The open form guides you through the process of using IActiveViewEvents.

Additional information
Note: If your ArcMap session is full screen you may not see the dialog that opens as a result of
clicking the newly added button. This is because the resulting dialog is not modal allowing you to
interact with ArcMap and see the events being fired as they occur. You may need to resize and
reposition ArcMap on your screen to see the Wiring Events dialog in action.

VB.NETC#

CommandWiringEvents.vb Launches Form1.vb and contains the hook into ArcGIS Desktop.

(view code)

Form1.vb Shows the workings of AddHandler and RemoveHandler for event wiring.

(view code)

CommandWiringEvents.cs Launches Form1.cs and contains the hook into ArcGIS Desktop.

(view code)
Form1.cs Shows the workings of += and -= for event wiring.

(view code)

Depending on what products you have installed and whether you installed the samples feature, you
will find the files associated with this sample in <Your ArcGIS install
location>\DeveloperKit\SamplesNET in the Desktop folder in the DesktopWiringEvents folder.

Wiring events in ArcGIS Desktop using IActiveViewEvents

CommandWiringEvents.vb

' Copyright 2008 ESRI

'

' All rights reserved under the copyright laws of the United States

' and applicable international laws, treaties, and conventions.

'

' You may freely redistribute and use this sample code, with or

' without modification, provided you include the original copyright

' notice and use restrictions.

'

' See use restrictions at <your ArcGIS install


location>/developerkit/userestrictions.txt.

'

Imports System.Runtime.InteropServices

Imports System.Drawing

Imports ESRI.ArcGIS.ADF.BaseClasses

Imports ESRI.ArcGIS.ADF.CATIDs

Imports ESRI.ArcGIS.Framework

Imports ESRI.ArcGIS.ArcMapUI

<ComClass(CommandWiringEvents.ClassId, CommandWiringEvents.InterfaceId,
CommandWiringEvents.EventsId)> _

Public NotInheritable Class CommandWiringEvents

Inherits BaseCommand

#Region "COM GUIDs"


' These GUIDs provide the COM identity for this class

' and its COM interfaces. If you change them, existing

' clients will no longer be able to access the class.

Public Const ClassId As String = "baa8a8b3-b6fd-4af3-ad4c-741565178897"

Public Const InterfaceId As String = "53e299c3-1be2-4248-bf0b-aeabdb9fb9b0"

Public Const EventsId As String = "64efdfbc-bdbe-4fd9-a409-0f993543fb6d"

#End Region

#Region "COM Registration Function(s)"

<ComRegisterFunction(), ComVisibleAttribute(False)> _

Public Shared Sub RegisterFunction(ByVal registerType As Type)

' Required for ArcGIS Component Category Registrar support

ArcGISCategoryRegistration(registerType)

'Add any COM registration code after the ArcGISCategoryRegistration()


call

End Sub

<ComUnregisterFunction(), ComVisibleAttribute(False)> _

Public Shared Sub UnregisterFunction(ByVal registerType As Type)

' Required for ArcGIS Component Category Registrar support

ArcGISCategoryUnregistration(registerType)

'Add any COM unregistration code after the


ArcGISCategoryUnregistration() call

End Sub

#Region "ArcGIS Component Category Registrar generated code"

Private Shared Sub ArcGISCategoryRegistration(ByVal registerType As Type)

Dim regKey As String = String.Format("HKEY_CLASSES_ROOT\CLSID\{{{0}}}",


registerType.GUID)
MxCommands.Register(regKey)

End Sub

Private Shared Sub ArcGISCategoryUnregistration(ByVal registerType As Type)

Dim regKey As String = String.Format("HKEY_CLASSES_ROOT\CLSID\{{{0}}}",


registerType.GUID)

MxCommands.Unregister(regKey)

End Sub

#End Region

#End Region

Public Shared m_mxApplication As ESRI.ArcGIS.ArcMapUI.IMxApplication

Public Shared m_application As ESRI.ArcGIS.Framework.IApplication

' A creatable COM class must have a Public Sub New()

' with no parameters, otherwise, the class will not be

' registered in the COM registry and cannot be created

' via CreateObject.

Public Sub New()

MyBase.New()

' TODO: Define values for the public properties

MyBase.m_category = "Samples ArcGIS"

MyBase.m_caption = "VB.NET Wiring Events"

MyBase.m_message = "VB.NET Wiring Events"

MyBase.m_toolTip = "VB.NET Wiring Events"

MyBase.m_name = "VB.NET Wiring Events"

Try
'TODO: change bitmap name if necessary

Dim bitmapResourceName As String = Me.GetType().Name + ".bmp"

MyBase.m_bitmap = New Bitmap(Me.GetType(), bitmapResourceName)

Catch ex As Exception

System.Diagnostics.Trace.WriteLine(ex.Message, "Invalid Bitmap")

End Try

End Sub

Public Overrides Sub OnCreate(ByVal hook As Object)

If Not (hook Is Nothing) Then

If TypeOf (hook) Is IMxApplication Then

m_mxApplication = CType(hook, IMxApplication)

End If

End If

m_application = CType(m_mxApplication,
ESRI.ArcGIS.Framework.IApplication)

End Sub

Public Overrides Sub OnClick()

Dim vbForm1 As New Form1

vbForm1.Show()

End Sub

End Class

Wiring events in ArcGIS Desktop using IActiveViewEvents

Form1.vb
' Copyright 2008 ESRI

'

' All rights reserved under the copyright laws of the United States

' and applicable international laws, treaties, and conventions.

'

' You may freely redistribute and use this sample code, with or

' without modification, provided you include the original copyright

' notice and use restrictions.

'

' See use restrictions at <your ArcGIS install


location>/developerkit/userestrictions.txt.

'

Imports ESRI.ArcGIS.ArcMapUI

Imports ESRI.ArcGIS.Carto

Imports ESRI.ArcGIS.Framework

Public Class Form1

' Notes:

' Variables are prefixed with an 'm_' denoting that they are member
variables.

' This means they are global in scope for this class.

' This member variable will hold the map's IActiveView Event Handler

Private m_activeViewEvents As ESRI.ArcGIS.Carto.IActiveViewEvents_Event

' A member variable to count how many events have fired

Private count As System.Int32

Private Sub btnStep1_Click(ByVal sender As System.Object, ByVal e As


System.EventArgs) Handles btnStep1.Click
' The appliocation variable is set by referencing the public shared
member variable in the

' CommandWiringEvents class. This is one way to pass variables between


classes in an application.

Dim application As IApplication = CommandWiringEvents.m_application

' Now drill into the ArcObjects to get set the map's IActiveView Event
Handler

Dim mxDocument As IMxDocument = TryCast(application.Document,


IMxDocument)

Dim activeView As IActiveView = mxDocument.ActiveView

Dim map As IMap = activeView.FocusMap

m_activeViewEvents = CType(map, IActiveViewEvents_Event)

'Clear out any displayed event names that get fired

TextBox3.Clear()

' Reset to the counter to display how many events have fired

count = 0

' ***** SECTION (Start): Set up the event handlers for all of the
IActiveViewEvents

' Notes:

' You set the member m_ActiveViewEvents.XXXXXXXXX EventHandlers

' for the specific events you want to capture. VB.NET uses the AddressOf

' operator to emulate 'C++' pointer like functionality.

' Create an instance of the delegate, add it to AfterDraw event

AddHandler m_activeViewEvents.AfterDraw, AddressOf


OnActiveViewEventsAfterDraw

' Create an instance of the delegate, add it to AfterItemDraw event


AddHandler m_activeViewEvents.AfterItemDraw, AddressOf
OnActiveViewEventsItemDraw

' Create an instance of the delegate, add it to ContentsChanged event

AddHandler m_activeViewEvents.ContentsChanged, AddressOf


OnActiveViewEventsContentsChanged

' Create an instance of the delegate, add it to ContentsCleared event

AddHandler m_activeViewEvents.ContentsCleared, AddressOf


OnActiveViewEventsContentsCleared

'Create an instance of the delegate, add it to FocusMapChanged event

AddHandler m_activeViewEvents.FocusMapChanged, AddressOf


OnActiveViewEventsFocusMapChanged

' Create an instance of the delegate, add it to ItemAdded event

AddHandler m_activeViewEvents.ItemAdded, AddressOf


OnActiveViewEventsItemAdded

' Create an instance of the delegate, add it to ItemDeleted event

AddHandler m_activeViewEvents.ItemDeleted, AddressOf


OnActiveViewEventsItemDeleted

'Create an instance of the delegate, add it to ItemReordered event

AddHandler m_activeViewEvents.ItemReordered, AddressOf


OnActiveViewEventsItemReordered

' Create an instance of the delegate, add it to SelectionChanged event

AddHandler m_activeViewEvents.SelectionChanged, AddressOf


OnActiveViewEventsSelectionChanged

' Create an instance of the delegate, add it to SpatialReferenceChanged


event

AddHandler m_activeViewEvents.SpatialReferenceChanged, AddressOf


OnActiveViewEventsSpatialReferenceChanged
' Create an instance of the delegate, add it to ViewRefreshed event

AddHandler m_activeViewEvents.ViewRefreshed, AddressOf


OnActiveViewEventsViewRefreshed

' ***** SECTION (End): Set up the event handlers for all of the
IActiveViewEvents

End Sub

Private Sub btnStep3_Click(ByVal sender As System.Object, ByVal e As


System.EventArgs) Handles btnStep3.Click

' ***** SECTION (Start): Remove Event Handlers

RemoveHandler m_activeViewEvents.AfterDraw, AddressOf


OnActiveViewEventsAfterDraw

RemoveHandler m_activeViewEvents.AfterItemDraw, AddressOf


OnActiveViewEventsItemDraw

RemoveHandler m_activeViewEvents.ContentsChanged, AddressOf


OnActiveViewEventsContentsChanged

RemoveHandler m_activeViewEvents.ContentsCleared, AddressOf


OnActiveViewEventsContentsCleared

RemoveHandler m_activeViewEvents.FocusMapChanged, AddressOf


OnActiveViewEventsFocusMapChanged

RemoveHandler m_activeViewEvents.ItemAdded, AddressOf


OnActiveViewEventsItemAdded

RemoveHandler m_activeViewEvents.ItemDeleted, AddressOf


OnActiveViewEventsItemDeleted

RemoveHandler m_activeViewEvents.ItemReordered, AddressOf


OnActiveViewEventsItemReordered

RemoveHandler m_activeViewEvents.SelectionChanged, AddressOf


OnActiveViewEventsSelectionChanged

RemoveHandler m_activeViewEvents.SpatialReferenceChanged, AddressOf


OnActiveViewEventsSpatialReferenceChanged

RemoveHandler m_activeViewEvents.ViewRefreshed, AddressOf


OnActiveViewEventsViewRefreshed

' ***** SECTION (End): Remove Event Handlers

End Sub
' ***** SECTION (Start): Custom Functions that you write to add additionaly
functionality for the events

' Event handler

Private Sub OnActiveViewEventsAfterDraw(ByVal Display As


ESRI.ArcGIS.Display.IDisplay, ByVal phase As
ESRI.ArcGIS.Carto.esriViewDrawPhase)

' Add your code here

' System.Windows.Forms.MessageBox.Show("AfterDraw")

' Tell which event fired

TextBox3.Text = TextBox3.Text + vbCrLf + "AfterDraw " + count.ToString

DoTextBoxMaintenance()

End Sub

' Event handler

Private Sub OnActiveViewEventsItemDraw(ByVal Index As Short, ByVal Display


As ESRI.ArcGIS.Display.IDisplay, ByVal phase As
ESRI.ArcGIS.esriSystem.esriDrawPhase)

' Add your code here

' System.Windows.Forms.MessageBox.Show("ItemDraw")

' Tell which event fired

TextBox3.Text = TextBox3.Text + vbCrLf + "ItemDraw " + count.ToString

DoTextBoxMaintenance()

End Sub

' Event handler

Private Sub OnActiveViewEventsContentsChanged()


' Add your code here

' System.Windows.Forms.MessageBox.Show("ContentsChanged")

' Tell which event fired

TextBox3.Text = TextBox3.Text + vbCrLf + "ContentsChanged " +


count.ToString

DoTextBoxMaintenance()

End Sub

' Event handler

Private Sub OnActiveViewEventsContentsCleared()

' Add your code here

' System.Windows.Forms.MessageBox.Show("ContentsCleared")

' Tell which event fired

TextBox3.Text = TextBox3.Text + vbCrLf + "ContentsCleared " +


count.ToString

DoTextBoxMaintenance()

End Sub

' Event handler

Private Sub OnActiveViewEventsFocusMapChanged()

' Add your code here

' System.Windows.Forms.MessageBox.Show("FocusMapChanged")
' Tell which event fired

TextBox3.Text = TextBox3.Text + vbCrLf + "FocusMapChanged " +


count.ToString

DoTextBoxMaintenance()

End Sub

' Event handler

Private Sub OnActiveViewEventsItemAdded(ByVal Item As Object)

' Add your code here

' System.Windows.Forms.MessageBox.Show("ItemAdded")

' Tell which event fired

TextBox3.Text = TextBox3.Text + vbCrLf + "ItemAdded " + count.ToString

DoTextBoxMaintenance()

End Sub

' Event handler

Private Sub OnActiveViewEventsItemDeleted(ByVal Item As Object)

' Add your code here

' System.Windows.Forms.MessageBox.Show("ItemDeleted")

' Tell which event fired

TextBox3.Text = TextBox3.Text + vbCrLf + "ItemDeleted " + count.ToString

DoTextBoxMaintenance()
End Sub

' Event handler

Private Sub OnActiveViewEventsItemReordered(ByVal Item As Object, ByVal


toIndex As Integer)

' Add your code here

' System.Windows.Forms.MessageBox.Show("ItemReordered")

' Tell which event fired

TextBox3.Text = TextBox3.Text + vbCrLf + "ItemReordered " +


count.ToString

DoTextBoxMaintenance()

End Sub

' Event handler

Private Sub OnActiveViewEventsSelectionChanged()

' Add your code here

' System.Windows.Forms.MessageBox.Show("SelectionChanged")

' Tell which event fired

TextBox3.Text = TextBox3.Text + vbCrLf + "SelectionChanged " +


count.ToString

DoTextBoxMaintenance()

End Sub

' Event handler

Private Sub OnActiveViewEventsSpatialReferenceChanged()


' Add your code here

' System.Windows.Forms.MessageBox.Show("SpatialReferenceChanged")

' Tell which event fired

TextBox3.Text = TextBox3.Text + vbCrLf + "SpatialReferenceChanged " +


count.ToString

DoTextBoxMaintenance()

End Sub

' Event handler

Private Sub OnActiveViewEventsViewRefreshed(ByVal view As


ESRI.ArcGIS.Carto.IActiveView, ByVal phase As
ESRI.ArcGIS.Carto.esriViewDrawPhase, ByVal data As Object, ByVal envelope As
ESRI.ArcGIS.Geometry.IEnvelope)

' Add your code here

' System.Windows.Forms.MessageBox.Show("ViewRefreshed")

' Tell which event fired

TextBox3.Text = TextBox3.Text + vbCrLf + "ViewRefreshed " +


count.ToString

DoTextBoxMaintenance()

End Sub

' ***** SECTION (End): Custom Functions that you write to add additionaly
functionality for the events

Private Sub DoTextBoxMaintenance()


' Scroll the textbox to the last entry

TextBox3.SelectionStart = TextBox3.Text.Length

TextBox3.ScrollToCaret()

' Increment the counter

count = count + 1

End Sub

End Class

Extension to listen to document open and save events

PurposeA component can listen to the events fired by the application when a document is created
and opened before closing, or closed. However, no event is fired when a document is saved. To
catch the time when a document is saved, create a custom extension that implements persistence.
This sample shows how to implement an application extension that logs user and date time
information when a document is opened or saved.

Development licensing Deployment licensing

ArcView ArcView

ArcEditor ArcEditor

ArcInfo ArcInfo

How to use
See Using the samples for help on compiling, setting up the debugger, and running the sample
(either an exe or dll).

1. Open and compile the sample project in Visual Studio; set up the debug application
if needed.
2. This sample works in ArcMap, ArcScene, or ArcGlobe; start any of the applications.
3. When a document is opened or saved, pay attention to the status bar for a
message with the user name and time information of the event.

C#VB.NET

LogExtension.cs Class implementing an extension that listens to document events.

(view code)

LogExtension.vb Class implementing an extension that listens to document events.


(view code)

Depending on what products you have installed and whether you installed the samples feature,
you will find the files associated with this sample in <Your ArcGIS install
location>\DeveloperKit\SamplesNET in the Desktop folder in the OpenSaveLogExtension folder.

Extension to listen to document open and save events

LogExtension.vb

' Copyright 2008 ESRI

'

' All rights reserved under the copyright laws of the United States

' and applicable international laws, treaties, and conventions.

'

' You may freely redistribute and use this sample code, with or

' without modification, provided you include the original copyright

' notice and use restrictions.

'

' See use restrictions at <your ArcGIS install


location>/developerkit/userestrictions.txt.

'

Imports ESRI.ArcGIS.ADF.CATIDs

Imports ESRI.ArcGIS.esriSystem

Imports System.Runtime.InteropServices

Imports ESRI.ArcGIS.Framework

Imports ESRI.ArcGIS.ArcMapUI

<ComClass(LogExtension.ClassId, LogExtension.InterfaceId,
LogExtension.EventsId), _

ProgId("OpenSaveLogExtensionVB.LogExtension")> _

Public Class LogExtension

Implements IExtension

Implements IPersistVariant

#Region "COM Registration Function(s)"


<ComRegisterFunction(), ComVisibleAttribute(False)> _

Public Shared Sub RegisterFunction(ByVal registerType As Type)

' Required for ArcGIS Component Category Registrar support

ArcGISCategoryRegistration(registerType)

'Add any COM registration code after the ArcGISCategoryRegistration()


call

End Sub

<ComUnregisterFunction(), ComVisibleAttribute(False)> _

Public Shared Sub UnregisterFunction(ByVal registerType As Type)

' Required for ArcGIS Component Category Registrar support

ArcGISCategoryUnregistration(registerType)

'Add any COM unregistration code after the


ArcGISCategoryUnregistration() call

End Sub

#Region "ArcGIS Component Category Registrar generated code"

''' <summary>

''' Required method for ArcGIS Component Category registration -

''' Do not modify the contents of this method with the code editor.

''' </summary>

Private Shared Sub ArcGISCategoryRegistration(ByVal registerType As Type)

Dim regKey As String = String.Format("HKEY_CLASSES_ROOT\CLSID\{{{0}}}",


registerType.GUID)

GMxExtensions.Register(regKey)

MxExtension.Register(regKey)

SxExtensions.Register(regKey)

End Sub
''' <summary>

''' Required method for ArcGIS Component Category unregistration -

''' Do not modify the contents of this method with the code editor.

''' </summary>

Private Shared Sub ArcGISCategoryUnregistration(ByVal registerType As Type)

Dim regKey As String = String.Format("HKEY_CLASSES_ROOT\CLSID\{{{0}}}",


registerType.GUID)

GMxExtensions.Unregister(regKey)

MxExtension.Unregister(regKey)

SxExtensions.Unregister(regKey)

End Sub

#End Region

#End Region

#Region "COM GUIDs"

' These GUIDs provide the COM identity for this class

' and its COM interfaces. If you change them, existing

' clients will no longer be able to access the class.

Public Const ClassId As String = "8545752a-d04c-4d92-84cd-65a5dd5f8de8"

Public Const InterfaceId As String = "fdd2bdab-568d-490c-9ca3-916195a88ad2"

Public Const EventsId As String = "07ba5ee7-1796-446f-947f-ad342153d3d4"

#End Region

Private m_application As IApplication

' A creatable COM class must have a Public Sub New()

' with no parameters, otherwise, the class will not be

' registered in the COM registry and cannot be created

' via CreateObject.

Public Sub New()


MyBase.New()

End Sub

#Region "Add Event Wiring for Open Documents"

'Event member variables

Private m_docEvents As IDocumentEvents_Event

'Wiring

Private Sub SetUpDocumentEvent(ByVal myDocument As IDocument)

m_docEvents = CType(myDocument, IDocumentEvents_Event)

AddHandler m_docEvents.OpenDocument, AddressOf OnOpenDocument

'Optional, new and close events

AddHandler m_docEvents.NewDocument, AddressOf OnNewDocument

AddHandler m_docEvents.CloseDocument, AddressOf OnCloseDocument

End Sub

Private Sub OnOpenDocument()

Debug.WriteLine("Open document", "Sample Extension (VB.Net)")

Dim logText As String = "Document '" + m_application.Document.Title +


"'" _

+ " opened by " + Environment.UserName _

+ " at " + DateTime.Now.ToLongTimeString()

LogMessage(logText)

End Sub

Private Sub OnNewDocument()

Debug.WriteLine("New document", "Sample Extension (VB.Net)")

End Sub

Private Sub OnCloseDocument()


Debug.WriteLine("Close document", "Sample Extension (VB.Net)")

End Sub

#End Region

#Region "IExtension Implementations"

Public ReadOnly Property Name() As String Implements


ESRI.ArcGIS.esriSystem.IExtension.Name

Get

Return "OpenSaveLogExtensionVB"

End Get

End Property

Public Sub Shutdown() Implements ESRI.ArcGIS.esriSystem.IExtension.Shutdown

m_docEvents = Nothing

m_application = Nothing

End Sub

Public Sub Startup(ByRef initializationData As Object) Implements


ESRI.ArcGIS.esriSystem.IExtension.Startup

m_application = TryCast(initializationData, IApplication)

SetUpDocumentEvent(m_application.Document)

End Sub

#End Region

#Region "IPersistVariant Implementations"

Public ReadOnly Property ID() As ESRI.ArcGIS.esriSystem.UID Implements


ESRI.ArcGIS.esriSystem.IPersistVariant.ID

Get

Dim extUID As New UIDClass()

extUID.Value = Me.GetType().GUID.ToString("B")

Return extUID

End Get

End Property
Public Sub Load(ByVal Stream As ESRI.ArcGIS.esriSystem.IVariantStream)
Implements ESRI.ArcGIS.esriSystem.IPersistVariant.Load

Marshal.ReleaseComObject(Stream)

End Sub

Public Sub Save(ByVal Stream As ESRI.ArcGIS.esriSystem.IVariantStream)


Implements ESRI.ArcGIS.esriSystem.IPersistVariant.Save

Debug.WriteLine("Save document", "Sample Extension (VB.Net)")

LogMessage("Document '" + m_application.Document.Title + "'" _

+ " saved by " + Environment.UserName _

+ " at " + DateTime.Now.ToLongTimeString())

Marshal.ReleaseComObject(Stream)

End Sub

#End Region

Private Sub LogMessage(ByVal message As String)

m_application.StatusBar.Message(0) = message

End Sub

End Class

You might also like