Suggested naming standards
Order of conditional determination
Multiple property operations
Ambiguous type matching
Simple image display
While Wend constructs
The Visual Basic Virtual Machine
Interacting with the IUnknown Interface
Working With HRESULTs
Working with properties
Working with methods
Working with events
Pointers to valid objects as parameters
Passing data between modules
Using the TypeOf keyword
Using the Is operator
Iterating through a collection
Consider preloading forms to increase the responsiveness of your application. Be careful not to preload too many (preloading three or four forms is fine).
Use resource files (.res) instead of external files when working with bitmap files, icons, and related files.
Make use of constructors and destructors to set variable references that are only set when the class is loaded. These are the VB functions: Class_Initialize and Class_Terminate or Form_Load and Form_Unload. Set all variables to Nothing when the object is destroyed.
Make sure the tab order is set correctly for the form. Do not add scroll bars to the tabbing sequence; it is too confusing.
Add access keys to those labels that identify controls of special importance on the form (use the TabIndex property).
Use system colors where possible instead of hard-coded colors.
This line causes count to be declared as a Variant, which is likely to be unintended.
Dim count, max As Long
This line declares both count and max as Long, the intended type.
Dim count As Long, max As Long
These lines also declare count and max as Long and are more readable.
Dim count As Long Dim max As Long
The tables below summarize suggested naming standards for the various elements of your Visual Basic projects.
Name your modules according to the overall function they provide; do not leave any with default names (such as, "Form1", "Class1", or "Module1"). In addition, prefix the names of forms, classes, and standard modules with three letters that denote the type of module, as shown in the table above.
|Rich text box||rtf|
As with modules, name your controls according to the function they provide; do not leave them with default names since this leads to decreased maintainability. Use the three-letter prefixes above to identify the type of control.
Use the following notation for naming variables and constants:
<name> describes how the variable is used or what it contains. The <scope> and <type> portions should always be lowercase, and the <name> should use mixed case.
|esriGeometry||ESRI Object Library|
|stdole||Standard OLE COM Library|
|<empty>||Simple variable data type|
|c||constant within a form or class|
|g||public variable defined in a class form or standard module|
|m||private variable defined in a class or form|
|by||byte or unsigned char|
Use parentheses to make operator precedence and logic comparison statements easier to read.
Result = ((x * 24) / (y / 12)) + 42 If ((Not pFoo Is Nothing) And (Counter > 200)) Then
Visual Basic, unlike languages such as C and C++, performs conditional tests on all parts of the condition, even if the first part of the condition is False. This means you must not perform conditional tests on objects and interfaces that had their validity tested in an earlier part of the conditional statement.
' The following line will raise a runtime error if pFoo is NULL If ((Not pFoo Is Nothing) And (TypeOf pFoo.Thing Is IBar)) then End If ' The correct way to test this code is If (Not pFoo Is Nothing) Then If (TypeOf pFoo.Thing Is IBar) Then ' Perform action on IBar thing of Foo End If End If
Use two spaces or a tab width of two for indentation. Since there is always only one editor for VB code, formatting is not as critical an issue as it is for C++ code.
Avoid using default properties except for the most common cases. They lead to decreased legibility.
When accessing intermodule data or functions, always qualify the reference with the module name. This makes the code more readable and results in more efficient runtime binding.
When performing multiple operations against different properties of the same object, use a With … End With statement. It is more efficient than specifying the object each time.
With frmHello .Caption = "Hello world" .Font = "Playbill" .Left = (Screen.Width - .Width) / 2 .Top = (Screen.Height - .Height) / 2 End With
For arrays, never change Option Base to anything other than zero, which is the default. Use LBound and UBound to iterate over all items in an array.
myArray = GetSomeArray For i = LBound(myArray) To UBound(myArray) MsgBox cstr(myArray(i)) Next I
Since And, Or, and Not, are bitwise operators, ensure that all conditions using them test only for Boolean values, unless, of course, bitwise semantics are what is intended.
If (Not pFoo Is Nothing) Then ' Valid Foo do something with it End If
Refrain from using type suffixes on variables or function names, such as myString$ or Right$(myString), unless they are needed to distinguish 16-bit from 32-bit numbers.
For ambiguous type matching, use explicit conversion operators, such as CSng, CDbl, and CStr, instead of relying on VB to pick which one will be used.
Use an ImageControl rather than a PictureBox for simple image display. It is much more efficient.
Always use On Error to ensure fault-tolerant code. For each function that does error checking, use On Error to jump to a single error handler for the routine that deals with all exceptional conditions that are likely to be encountered. After the error handler processes the error—usually by displaying a message—it should proceed by issuing one of the recovery statements shown on the table below.
|Exit Sub||usually||Function failed, pass control back to caller|
|Raise||often||Raise a new error code in the caller's scope|
|Resume||rarely||Error condition removed, reattempt offending statement|
|Resume Next||very rarely||Ignore error and continue with the next statement|
Recovery statements issued by error handlers
Error handling in Visual Basic is not the same as general error handling in COM (see the section Working With HRESULTs).
Refrain from placing more than a few lines of code in event functions to prevent highly fractured and unorganized code. Event functions should simply dispatch to reusable functions elsewhere.
To ensure efficient use of memory resources, the following points should be considered:
Avoid While … Wend constructs. Use the Do While … Loop or Do Until ... Loop instead because you can conditionally branch out of this construct.
pFoos.Reset Set pFoo = pFoos.Next Do While (Not pFoo Is Nothing) If (pFoo.Answer = "Done") Then Exit Loop Set pFoo = pFoos.Next Loop
The Visual Basic Virtual Machine (VBVM) contains the intrinsic Visual Basic controls and services, such as starting and ending a Visual Basic application, required to successfully execute all Visual Basic developed code.
The VBVM was called the VB Runtime in earlier versions of the software.
The VBVM is packaged as a DLL that must be installed on any machine wanting to execute code written with Visual Basic, even if the code has been compiled to native code. If the dependencies of any Visual Basic compiled file are viewed, the file msvbvm60.dll is listed; this is the DLL housing the Virtual Machine.
The topic on COM contains a lengthy overview of the IUnknown interface and how it forms the basis on which all of COM is built. Visual Basic hides this interface from developers and performs the required interactions (QueryInterface, AddRef, and Release function calls) on the developer's behalf. It achieves this because of functionality contained within the VBVM. This simplifies development with COM for many developers, but to work successfully with ArcObjects, you must understand what the VBVM is doing.
Visual Basic developers are accustomed to dimensioning variables as follows:
Dim pColn as New Collection 'Create a new collection object PColn.Add "Foo", "Bar" 'Add element to collection
It is worth considering what is happening at this point. From a quick inspection of the code, it appears that the first line creates a collection object and gives the developer a handle on that object in the form of pColn. The developer then calls a method on the object Add. In the Introduction to COM topic you learned that objects talk via their interfaces, never through a direct handle on the object itself. Remember, objects expose their services via their interfaces. If this is true, something isn't adding up.
What is actually happening is some "VB magic" performed by the VBVM and some trickery by the Visual Basic Editor (VBE) in the way that it presents objects and interfaces. The first line of code instantiates an instance of the collection class, then assigns the default interface for that object, _Collection, to the variable pColn. It is this interface, _Collection, that has the methods defined on it. Visual Basic has hidden the interface-based programming to simplify the developer experience. This is not an issue if all the functionality implemented by the object can be accessed via one interface, but it is an issue when there are multiple interfaces on an object that provides services.
VBE backs this up by hiding default interfaces from the IntelliSense completion list and the object browser. By default, any interfaces that begin with an underscore, "_", are not displayed in the object browser (to display these interfaces, turn Show Hidden Member on, although this will still not display default interfaces).
You have already learned that the majority of ArcObjects have IUnknown as their default interface and that Visual Basic does not expose any of IUnknown's methods, namely QueryInterface, AddRef, and Release. Assume you have a class Foo that supports three interfaces, IUnknown (the default interface), IFoo, and IBar. This means that if you were to dimension the variable pFoo as below, the variable pFoo would point to the IUnknown interfaces.
Dim pFoo As New Foo ' Create a new Foo object pFoo.??????
Since Visual Basic does not allow direct access to the methods of IUnknown, you would immediately have to QI for an interface with methods on it that you can call. Because of this, the correct way to dimension a variable that will hold pointers to interfaces is as follows:
Dim pFoo As IFoo ' Variable will hold pointer to IFoo interface Set pFoo = New Foo ' Create Instance of Foo object and QI for IFoo
Now that you have a pointer to one of the object's interfaces, it is an easy matter to request from the object any of its other interfaces.
Dim pBar as IBar 'Dim variable to hold pointer to interface Set pBar = pFoo 'QI for IBar interface
By convention, most classes have an interface with the same name as the class with an "I" prefix; this tends to be the interface most commonly used when working with the object. You are not restricted to which interface you request when instantiating an object. Any supported interface can be requested; hence, the code below is valid.
Dim pBar as IBar Set pBar = New Foo 'CoCreate Object Set pFoo = pBar 'QI for interface
Objects control their own lifetime, which requires clients to call AddRef anytime an interface pointer is duplicated by assigning it to another variable and to call Release anytime the interface pointer is no longer required. Ensuring that there are a matching number of AddRefs and Releases is important, and fortunately, Visual Basic performs these calls automatically. This ensures that objects do not "leak". Even when interface pointers are reused, Visual Basic will correctly call release on the old interface before assigning the new interface to the variable. The following code illustrates these concepts; note the reference count on the object at the various stages of code execution.
Private Sub VBMagic() ' Dim a variable to the IUnknown interface on the simple object Dim pUnk As IUnknown ' Co Create simpleobject asking for the IUnknown interface Set pUnk = New SimpleObject 'refCount = 1 ' QI for a useful interface ' Define the interface Dim pMagic As ISimpleObject ' Perform the QI operation Set pMagic = punk 'refCount = 2 ' Dim another variable to hold another interface on the object Dim pMagic2 As IAnotherInterface ' QI for that interface Set pMagic2 = pMagic 'refCount = 3 ' Release the interface pointer Set pMagic2 = Nothing 'refCount = 2 ' Release the interface Set pMagic = Nothing 'refCount = 1 ' Now reuse the pUnk variable - what will VB do for this? Set pUnk = New SimpleObject 'refCount = 1, then 0, then 1 ' Let the interface variable go out of scope and let VB tidy up End Sub 'refCount = 0
See Visual Basic Magic sample in the server guide samples on the disk for this code. You are encouraged to run the sample and use the code. This object also uses an ATL C++ project to define the SimpleObject and its interfaces; you are encouraged to look at this code to learn a simple implementation of a C++ ATL object.
Often interfaces have properties that are actually pointers to other interfaces. Visual Basic allows you to access these properties in a shorthand fashion by chaining interfaces together. For instance, assume that you have a pointer to the IFoo interface, and that interface has a property called Gak that is an IGak interface with the method DoSomething(). You have a choice on how to access the DoSomething method. The first method is the long-handed way.
Dim pGak as IGak Set pGak = pFoo 'Assign IGak interface to local variable pGak.DoSomething 'Call method on IGak interface
Alternatively, you can chain the interfaces and accomplish the same thing on one line of code.
pFoo.Gak.DoSomething 'Call method on IGak interface
When looking at the sample code, you will see both methods. Normally, the former method is used on the simpler samples, as it explicitly tells you what interfaces are being worked with. More complex samples use the shorthand method.
This technique of chaining interfaces together can always be used to get the value of a property, but it cannot always be used to set the value of a property. Interface chaining can only be used to set a property if all the interfaces in the chain are set by reference. For instance, the code below would execute successfully.
MapControl1.Map.Layers(0).Name = "Foo"
The above example works because both the Layer of the Map and the Map of the map control are returned by reference. The lines of code below would not work since the Extent envelope is set by value on the active view.
MapControl1.ActiveView.Extent.Width = 32
The reason that this does not work is that the VBVM expands the interface chain to get the end property. Because an interface in the chain is dealt with by value, the VBVM has its own copy of the variable, not the one chained. To set the Width property of the extent envelope in the above example, the VBVM must write code similar to this:
Dim pActiveView as IActiveView Set pActiveView = MapControl1.ActiveView Dim pEnv as IEnvelope Set pEnv = pActiveView.Extent ' This is a get by value, PEnv.Width = 32 ' The VBVM has set its copy of the Extent and not ' the copy inside the ActiveView
For this to work the VBVM requires the extra line below.
pActiveView.Extent = pEnv ' This is a set by value
You will now see some specific uses of the create instance and query interface operations that involve ArcObjects. To use an ArcGIS object in Visual Basic or VBA, you must first reference the ESRI library that contains that object. If you are using VBA inside ArcMap or ArcCatalog, most of the common ESRI object libraries are already referenced for you. In standalone Visual Basic applications or components, you will have to manually reference the required libraries.
To find out what library an ArcObjects component is in, review the object model diagrams in the developer help or use the LibraryLocator tool in your developer kit tools directory.
You will start by identifying a simple object and an interface that it supports. In this case, you will use a Point object and the IPoint interface. One way to set the coordinates of the point is to invoke the PutCoords method on the IPoint interface and pass in the coordinate values.
Dim pPt As IPoint Set pPt = New Point pPt.PutCoords 100, 100
The first line of this simple code fragment illustrates the use of a variable to hold a reference to the interface that the object supports. The line reads the IID for the IPoint interface from the ESRI object library.
IID is short for interface identifier, a GUID.You may find it less ambiguous (as per the coding guidelines), particularly if you reference other object libraries in the same project, to precede the interface name with the library name, for example:
Dim pPt As esriGeometry.IPoint
That way, if there happens to be another IPoint referenced in your project, there won't be any ambiguity as to which one you are referring to.
The second line of the fragment creates an instance of the object or coclass, then performs a QI operation for the IPoint interface that it assigns to pPt.
A QI is required since the default interface of the object is IUnknown. Since the pPt variable was declared as type IPoint, the default IUnknown interface was QI'd for the IPoint interface.
Coclass is an abbreviation of component object class.
With a name for the coclass as common as Point, you may want to precede the coclass name with the library name, for example:
Set pPt = New esrigeometry.Point
The last line of the code fragment invokes the PutCoords method. If a method can't be located on the interface, an error will be shown at compile time.
This is the compilation error message shown when a method or property is not found on an interface.
So far, you have seen that all COM methods signify success or failure via an HRESULT that is returned from the method; no exceptions are raised outside the interface. You have also learned that Visual Basic raises exceptions when errors are encountered. In Visual Basic, HRESULTs are never returned from method calls, and to confuse you further when errors do occur, Visual Basic throws an exception. How can this be? The answer lies with the Visual Basic Virtual Machine. It is the VBVM that receives the HRESULT; if this is anything other than S_OK, the VBVM throws the exception. If it was able to retrieve any worthwhile error information from the COM error object, it populates the Visual Basic Err object with that information. In this way, the VBVM handles all HRESULTs returned from the client.
When implementing interfaces in Visual Basic, it is good coding practice to raise an HRESULT error to inform the caller that an error has occurred. Normally, this is done when a method has not been implemented.
' Defined in Module Const E_NOTIMPL = &H80004001 'Constant that represents HRESULT 'Added to any method not implemented On Error GoTo 0 Err.Raise E_NOTIMPL
You must also write code to handle the possibility that an HRESULT other than S_OK is returned. When this happens, an error handler should be called and the error dealt with. This may mean simply notifying the user, or it may mean automatically dealing with the error and continuing with the function. The choice depends on the circumstances. Below is a simple error handler that will catch any error that occurs within the function and report it to the user. Note the use of the Err object to provide the user with some description of the error.
Private Sub Test() On Error GoTo ErrorHandler ' Do something here Exit Sub ' Must exit sub here before error handler ErrorHandler: Msgbox "Error In Application – Description " & Err.Description End Sub
Some properties refer to specific interfaces in the ESRI object library, and other properties have values that are standard data types such as strings, numeric expressions, and Boolean values. For interface references, declare an interface variable and use the Set statement to assign the interface reference to the property. For other values, declare a variable with an explicit data type or use Visual Basic's Variant data type. Then, use a simple assignment statement to assign the value to the variable.
Properties that are interfaces can be set either by reference or by value. Properties that are set by value do not require the Set statement.
Dim pEnv As IEnvelope Set pEnv = pActiveView.Extent 'Get extent property of view pEnv.Expand 0.5, 0.5, True 'Shrink envelope pActiveView.Extent = pEnv 'Set By Value extent back on IActiveView Dim pFeatureLayer as IfeatureLayer Set pFeatureLayer = New FeatureLayer 'Create New Layer Set pFeatureLayer.FeatureClass = pClass 'Set ByRef a class into layer
As you might expect, some properties are read-only, others are write-only, and still others are read/write. All the object browsers and the ArcObjects Class Help (found in the ArcGIS Developer Help system) provide this information. If you attempt to use a property and either forget or misuse the Set keyword, Visual Basic will fail the compilation of the source code with a "method or data member not found" error message. This error may seem strange since it may be given for trying to assign a value to a read-only property. The reason for the message is that Visual Basic is attempting to find a method in the type library that maps to the property name. In the above examples, the underlying method calls in the type library are put_Extent and putref_FeatureClass.
Methods perform some action and may or may not return a value. In some instances, a method returns a value that's an interface; for example, in the code fragment below, TrackCircle returns an IPolygon interface:
Dim pCircle as IPolygon Set pCircle = MapControl1.TrackCircle
In other instances, a method returns a Boolean value that reflects the success of an operation or writes data to a parameter; for example, the IsActive method of IActiveView returns a value of true if the map is active.
Be careful not to confuse the idea of a Visual Basic return value from a method call with the idea that all COM methods must return an HRESULT. The VBVM is able to read type library information and set up the return value of the VB method call to be the appropriate parameter of the COM method.
Events let you know when something has occurred. You can add code to respond to an event. For example, a command button has a Click event. You add code to perform some action when the user clicks the control. You can also add events that certain objects generate. VBA and Visual Basic let you declare a variable with the keyword WithEvents. WithEvents tells the development environment that the object variable will be used to respond to the object's events. This is sometimes referred to as an "event sink". The declaration must be made in a class module or a form. Here's how you declare a variable and expose the events of an object in the Declarations section:
Private WithEvents m_pViewEvents as Map
Visual Basic only supports one outbound interface (marked as the default outbound interface in the IDL) per coclass. To get around this limitation, the coclasses that implement more than one outbound interface have an associated dummy coclass that allows access to the secondary outbound interface. These coclasses have the same name as the outbound interface they contain, minus the I.
Private WithEvents m_pMapEvents as MapEvents
Once you've declared the variable, search for its name in the Object combo box at the top left of the Code window. Then, inspect the list of events to which you can attach code in the Procedure/Events combo box at the top right of the code window.
Not all procedures of the outbound event interface need to be stubbed out, as Visual Basic will stub out any unimplemented methods. This is different from inbound interfaces, in which all methods must be stubbed out for compilation to occur.
Before the methods are called, the hookup between the event source and sink must be made. This is done by setting the variable that represents the sink to the event source.
Set m_pMapEvents = MapControl1.Map
Some ArcGIS methods expect interfaces for some of their parameters. The interface pointers passed can point to an instanced object before the method call or after the method call is completed.
For example, if you have a polygon (pPolygon) whose center point you want to find, you can write code as follows:
Dim pArea As IArea Dim pPt As IPoint Set pArea = pPolygon ' QI for IArea on pPolygon Set pPt = pArea.Center
You don't need to create pPt because the Center method creates a Point object for you and passes back a reference to the object via its IPoint interface. Only methods that use client-side storage require you to create the object prior to the method call.
When passing data between modules it is best to use accessor and mutator functions that manipulate some private member variable. This provides data encapsulation, which is a fundamental technique in object-oriented programming. Public variables should never be used.
For instance, you might have decided that a variable has a valid range of 1–100. If you were to allow other developers direct access to that variable, they could set the value to an illegal value. The only way of coping with these illegal values is to check them before they get used. This is both error prone and tiresome to program. The technique of declaring all variables as private member variables of the class and providing accessor and mutator functions for manipulating these variables will solve this problem.
In the example below, these properties are added to the default interface of the class. Notice the technique used to raise an error to the client.
Private m_lPercentage As Long Public Property Get Percentage() As Long Percentage = m_lPercentage End Property Public Property Let Percentage(ByVal lNewValue As Long) If (lNewValue >= 0) And (lNewValue <= 100) Then m_lPercentage = lNewValue Else Err.Raise vbObjectError + 29566, "MyProj.MyObject", _ "Invalid Percentage Value. Valid values (0 -> 100)" End If End Property
When you write code to pass an object reference from one form, class, or module to another, for example:
Private Property Set PointCoord(ByRef pPt As IPoint) Set m_pPoint = pPt End Property
your code passes a pointer to an instance of the IPoint interface. This means that you are only passing the reference to the interface, not the interface itself; if you add the ByVal keyword (as follows), the interface is passed by value.
Private Property Let PointCoord(ByVal pPt As IPoint) Set m_pPoint = pPt End Property
In both of these cases the object pointed to by the interfaces is always passed by reference. To pass the object by value, a clone of the object must be made, and that clone is passed.
To check whether an object supports an interface, you can use Visual Basic's TypeOf keyword. For example, given the first layer in a map control, you can test whether it is a CAD layer using the following code:
Dim pUnk As IUnknown Dim pCadLayer As ICadLayer Set pUnk = MapControl1.Layer(0) If TypeOf pUnk Is ICadLayer Then Set pCadLayer = pUnk ' do something with the layer End If
If your code requires you to compare two interface reference variables, you can use the Is operator. Typically, you can use the Is operator in the following circumstances:
To check if you have a valid interface. For example, see the following code:
Dim pPt As IPoint Set pPt = New Point If (Not pPt Is Nothing) Then 'a valid pointer? ... ' do something with pPt End If
To check if two interface variables refer to the same actual object. Imagine that you have two interface variables of type IPoint, pPt1 and pPt2. Are they pointing to the same object? If they are, then pPt1 Is pPt2.
The Is keyword works with the COM identity of an object. Below is an example that illustrates the use of the Is keyword when finding out if a certain method on an interface returns a copy of or a reference to the same real object.
In the following example, the Extent property on a map (IMap) returns a copy, while the ActiveView property on a MapControl always returns a reference to the real object.
Dim pEnv1 As IEnvelope Dim pEnv2 as IEnvelope Dim pActiveView1 As IActiveView Dim pActiveView2 as IActiveView Set pEnv1 = MapControl1.ActiveView.Extent Set pEnv2 = MapControl1.ActiveView.Extent Set pActiveView1 = MapControl1.ActiveView Set pActiveView2 = MapControl1.ActiveView ' Extent returns a copy, ' so pEnv1 Is pEnv2 returns false MsgBox pEnv1 Is pEnv2 ' ActiveView returns a reference, so ' ActiveView1 Is ActiveView2 returns true MsgBox pActiveView1 Is pActiveView2
In your work with ArcObjects, you'll discover that, in many cases, you'll be working with collections. You can iterate through these collections with an enumerator. An enumerator is an interface that provides methods for traversing a list of elements. Enumerator interfaces typically begin with IEnum and have two methods, Next and Reset. Next returns the next element in the set and advances the internal pointer, and Reset resets the internal pointer to the beginning.
Enumerators can support other methods, but these two methods are common among all enumerators.
Here is some VB code that loops through the selected features (IEnumFeature) in a map control.
Some collection objects, the Visual Basic Collection being one, implement a special interface called _NewEnum. This interface, because of the _ prefix, is hidden, but Visual Basic developers can still use it to simplify iterating though a collection. The Visual Basic For Each construct works with this interface to perform the Reset and Next steps through a collection.
Dim pEnumFeat As IEnumFeature Dim pFeat As IFeature Set pEnumFeat = MapControl1.Map.FeatureSelection Set pFeat = pEnumFeat.Next Do While (Not pFeat Is Nothing) Debug.Print pFeat.Value(pFeat.Fields.FindField("state_name")) Set pFeat = pEnumFeat.Next Loop
Dim pColn as Collection Set pColn = GetCollection() ' Collection returned from some function Dim thing as Variant ' VB uses methods on _NewEnum to step through For Each thing in pColn ' an enumerator. MsgBox Cstr(thing) Next