ARCGIS_DEVELOPMENT

182
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.

Transcript of ARCGIS_DEVELOPMENT

Page 1: ARCGIS_DEVELOPMENT

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

Page 2: ARCGIS_DEVELOPMENT

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#]

Page 3: ARCGIS_DEVELOPMENT

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();

Page 4: ARCGIS_DEVELOPMENT

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: 

Page 5: ARCGIS_DEVELOPMENT

[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:  

Page 6: ARCGIS_DEVELOPMENT

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

{

Page 7: ARCGIS_DEVELOPMENT

//

}

[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#]

Page 8: ARCGIS_DEVELOPMENT

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

Page 9: ARCGIS_DEVELOPMENT

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

Page 10: ARCGIS_DEVELOPMENT

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

Page 11: ARCGIS_DEVELOPMENT

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

Page 12: ARCGIS_DEVELOPMENT

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

Page 13: ARCGIS_DEVELOPMENT

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.

Page 14: ARCGIS_DEVELOPMENT

 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 EngineThe 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.

Page 15: ARCGIS_DEVELOPMENT

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

Page 16: ARCGIS_DEVELOPMENT

[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#]

Page 17: ARCGIS_DEVELOPMENT

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:

 

Page 18: ARCGIS_DEVELOPMENT

 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.GetManifestResourceStream(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.

Page 19: ARCGIS_DEVELOPMENT

 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.

Page 20: ARCGIS_DEVELOPMENT

 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(

Page 21: ARCGIS_DEVELOPMENT

"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

Page 22: ARCGIS_DEVELOPMENT

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 localizationThe 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:

Page 23: ARCGIS_DEVELOPMENT

 [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);

}

Page 24: ARCGIS_DEVELOPMENT

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++]

Page 25: ARCGIS_DEVELOPMENT

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

Page 26: ARCGIS_DEVELOPMENT

{  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

Page 27: ARCGIS_DEVELOPMENT

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

Page 28: ARCGIS_DEVELOPMENT

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

Page 29: ARCGIS_DEVELOPMENT

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

Page 30: ARCGIS_DEVELOPMENT

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;

}

Page 31: ARCGIS_DEVELOPMENT

[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;

Page 32: ARCGIS_DEVELOPMENT

//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: 

Page 33: ARCGIS_DEVELOPMENT

 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

Page 34: ARCGIS_DEVELOPMENT

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]

Page 35: ARCGIS_DEVELOPMENT

'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;

Page 36: ARCGIS_DEVELOPMENT

}

[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. 

Page 37: ARCGIS_DEVELOPMENT

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

Page 38: ARCGIS_DEVELOPMENT

'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. 

Page 39: ARCGIS_DEVELOPMENT

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

Page 40: ARCGIS_DEVELOPMENT

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 extensionsDuring 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. ObjectStreamsAn 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.  

Page 41: ARCGIS_DEVELOPMENT

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

Page 42: ARCGIS_DEVELOPMENT

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 IPersistVariantThis 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 & "}"

Page 43: ARCGIS_DEVELOPMENT

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

Page 44: ARCGIS_DEVELOPMENT

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

{

Page 45: ARCGIS_DEVELOPMENT

//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)

Page 46: ARCGIS_DEVELOPMENT

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();

Page 47: ARCGIS_DEVELOPMENT

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. 

Page 48: ARCGIS_DEVELOPMENT

Unregistered classesYou 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. 

Page 49: ARCGIS_DEVELOPMENT

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

Page 50: ARCGIS_DEVELOPMENT

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);

Page 51: ARCGIS_DEVELOPMENT

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

Page 52: ARCGIS_DEVELOPMENT

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())

Page 53: ARCGIS_DEVELOPMENT

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);

Page 54: ARCGIS_DEVELOPMENT

� � �

//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();

}

}

Page 55: ARCGIS_DEVELOPMENT

[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: 

Page 56: ARCGIS_DEVELOPMENT

   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. 

Page 57: ARCGIS_DEVELOPMENT

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

Page 58: ARCGIS_DEVELOPMENT

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();

Page 59: ARCGIS_DEVELOPMENT

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)

Page 60: ARCGIS_DEVELOPMENT

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 persistRemember 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 integrityIf 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 consistencyYou 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.

Page 61: ARCGIS_DEVELOPMENT

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

Page 62: ARCGIS_DEVELOPMENT

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();

Page 63: ARCGIS_DEVELOPMENT

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: 

Page 64: ARCGIS_DEVELOPMENT

 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:  

Page 65: ARCGIS_DEVELOPMENT

  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

Page 66: ARCGIS_DEVELOPMENT

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

Page 67: ARCGIS_DEVELOPMENT

 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

Page 68: ARCGIS_DEVELOPMENT

 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.

← 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.

← 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.

Page 69: ARCGIS_DEVELOPMENT

← 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);

}

Page 70: ARCGIS_DEVELOPMENT

[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.

Page 71: ARCGIS_DEVELOPMENT

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

Page 72: ARCGIS_DEVELOPMENT

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();

Page 73: ARCGIS_DEVELOPMENT

/// <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;

Page 74: ARCGIS_DEVELOPMENT

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();

Page 75: ARCGIS_DEVELOPMENT

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)

{

// ...

}

}

Page 76: ARCGIS_DEVELOPMENT

}

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

Page 77: ARCGIS_DEVELOPMENT

// 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);

}

...

Page 78: ARCGIS_DEVELOPMENT

// 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

Page 79: ARCGIS_DEVELOPMENT

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

{

Page 80: ARCGIS_DEVELOPMENT

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

Page 81: ARCGIS_DEVELOPMENT

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>

Page 82: ARCGIS_DEVELOPMENT

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. 

Page 83: ARCGIS_DEVELOPMENT

  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

Page 84: ARCGIS_DEVELOPMENT

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

}

);

}

Page 85: ARCGIS_DEVELOPMENT

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

Page 86: ARCGIS_DEVELOPMENT

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;

}

Page 87: ARCGIS_DEVELOPMENT

/// <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);

Page 88: ARCGIS_DEVELOPMENT

//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)

Page 89: ARCGIS_DEVELOPMENT

{

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);

}

Page 90: ARCGIS_DEVELOPMENT

/// <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);

Page 91: ARCGIS_DEVELOPMENT

...

}

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.

Page 92: ARCGIS_DEVELOPMENT

← 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

Page 93: ARCGIS_DEVELOPMENT

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")

Page 94: ARCGIS_DEVELOPMENT

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. 

Page 95: ARCGIS_DEVELOPMENT

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();

Page 96: ARCGIS_DEVELOPMENT

// 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++;

Page 97: ARCGIS_DEVELOPMENT

}

}

[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);

Page 98: ARCGIS_DEVELOPMENT

}

[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:

Page 99: ARCGIS_DEVELOPMENT

[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#]

Page 100: ARCGIS_DEVELOPMENT

// 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: 

Page 101: ARCGIS_DEVELOPMENT

  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: 

Page 102: ARCGIS_DEVELOPMENT

 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

{

Page 103: ARCGIS_DEVELOPMENT

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)

Page 104: ARCGIS_DEVELOPMENT

{

// 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. 

Page 105: ARCGIS_DEVELOPMENT

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()

Page 106: ARCGIS_DEVELOPMENT

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;

}

...

Page 107: ARCGIS_DEVELOPMENT

[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;

Page 108: ARCGIS_DEVELOPMENT

}

...

[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.

Page 109: ARCGIS_DEVELOPMENT

 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

Page 110: ARCGIS_DEVELOPMENT

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:

Page 111: ARCGIS_DEVELOPMENT

 

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.

Page 112: ARCGIS_DEVELOPMENT

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:

Page 113: ARCGIS_DEVELOPMENT

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:

Page 114: ARCGIS_DEVELOPMENT

 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

Page 115: ARCGIS_DEVELOPMENT

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:

 

Page 116: ARCGIS_DEVELOPMENT

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: 

Page 117: ARCGIS_DEVELOPMENT

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,

Page 118: ARCGIS_DEVELOPMENT

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.

Page 119: ARCGIS_DEVELOPMENT

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 Name

Value Action

EnableErrorReport DWORD (0 or 1)

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

ShowErrorDialog DWORD (0 or 1)

If set to 0, no dialog will be shown. The error report is saved to 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 [email protected]). 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 or 1)

If set to 1 send the error reports to the web service. The error report is saved to your local disk.

CacheSize DWORD (0 to 100)

The number of error reports to save on your disk. The error report is saved to your local disk.

 

How to wire ArcObjects .NET events

Page 120: ARCGIS_DEVELOPMENT

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;

Page 121: ARCGIS_DEVELOPMENT

[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

Page 122: ARCGIS_DEVELOPMENT

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;

Page 123: ARCGIS_DEVELOPMENT

((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#]

Page 124: ARCGIS_DEVELOPMENT

//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

Page 125: ARCGIS_DEVELOPMENT

'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

Page 126: ARCGIS_DEVELOPMENT

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;

Page 127: ARCGIS_DEVELOPMENT

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

Page 128: ARCGIS_DEVELOPMENT

'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

Page 129: ARCGIS_DEVELOPMENT

← 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

Page 130: ARCGIS_DEVELOPMENT

← 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(){}

Page 131: ARCGIS_DEVELOPMENT

← 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

Page 132: ARCGIS_DEVELOPMENT

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

Page 133: ARCGIS_DEVELOPMENT

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:  

Page 134: ARCGIS_DEVELOPMENT

 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.

Page 135: ARCGIS_DEVELOPMENT

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

Page 136: ARCGIS_DEVELOPMENT

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);

}

Page 137: ARCGIS_DEVELOPMENT

[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)

{

Page 138: ARCGIS_DEVELOPMENT

// 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

Page 139: ARCGIS_DEVELOPMENT

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.NET C#

CommandWiringEvents.vb

(view code)

Launches Form1.vb and contains the hook into ArcGIS Desktop.

Form1.vb

(view code)

Shows the workings of AddHandler and RemoveHandler for event wiring.

CommandWiringEvents.cs

(view code)

Launches Form1.cs and contains the hook into ArcGIS Desktop.

Form1.cs

(view code)

Shows the workings of += and -= for event wiring.

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

Page 140: ARCGIS_DEVELOPMENT

'

' 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

Page 141: ARCGIS_DEVELOPMENT

#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)

Page 142: ARCGIS_DEVELOPMENT

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

Page 143: ARCGIS_DEVELOPMENT

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

Page 144: ARCGIS_DEVELOPMENT

' 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

Page 145: ARCGIS_DEVELOPMENT

' 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

Page 146: ARCGIS_DEVELOPMENT

' 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

Page 147: ARCGIS_DEVELOPMENT

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)

Page 148: ARCGIS_DEVELOPMENT

' 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")

Page 149: ARCGIS_DEVELOPMENT

' 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()

Page 150: ARCGIS_DEVELOPMENT

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)

Page 151: ARCGIS_DEVELOPMENT

' 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")

Page 152: ARCGIS_DEVELOPMENT

' 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

Page 153: ARCGIS_DEVELOPMENT

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

(view code)

Class implementing an extension that listens to document events.

LogExtension.vb

(view code)

Class implementing an extension that listens to document events.

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

Page 154: ARCGIS_DEVELOPMENT

'

' 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

Page 155: ARCGIS_DEVELOPMENT

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)

Page 156: ARCGIS_DEVELOPMENT

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

Page 157: ARCGIS_DEVELOPMENT

'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

Page 158: ARCGIS_DEVELOPMENT

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

Page 159: ARCGIS_DEVELOPMENT

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