Lesson 5: Being Selective
In the next few lessons you will modify the code you wrote in Lesson 1 to add some additional functionality to your plug-in. In this lesson you will modify your code to prompt the user for some information while your custom command is running. Instead of overruling the TransformBy behavior of every block attribute (AttributeReference object) in the drawing, you will ask the user to select a block insert (BlockReference) containing the AttributeReferences they want to overrule, and then you will use that user selection to apply a filter to your overrule.
If you closed Visual Basic Express after the last lesson, re-launch it now and open your KeepAttributesHorizontal project. Press <Ctrl+Shift+F9> to remove any breakpoints you had set in the project.
In the code snippets that follow, we will show new code you must add in bold, and code you must delete in strikethrough.
Asking the user for input requires some classes you’ve not used before, and these are contained in two new namespaces in the AutoCAD API. Add Imports directives for the two new namespaces below the three Imports directives you already have at the top of MyCommand.vb:
Now add the following code to ImplementOverrule() method. As before, we recommend you type this code out (rather than copying and pasting it) so you experience the Visual Studio IntelliSense in action:
Public Shared Sub ImplementOverrule()
Dim doc As Document =
Dim ed As Editor = doc.Editor
' Select a block reference
Dim opts As New PromptEntityOptions(
vbLf & "Select a block reference:")
opts.SetRejectMessage(vbLf & "Must be block reference...")
Dim res As PromptEntityResult = ed.GetEntity(opts)
If res.Status <> PromptStatus.OK Then
This code will prompt the user for some information, but you’re not doing anything with that information yet. Nevertheless, now is a good time to test how this code makes AutoCAD behave.
- Press F5 to launch AutoCAD from your debugger.
- NETLOAD the compiled DLL and load the sample drawing.
- Set a breakpoint at the start of the ImplementOverrule() method so you can step through your code and examine the PromptEntityResults object returned from your call to GetEntity().
- Run the KEEPSTRAIGHT command a few times and test the following:
- Select a BlockReference at the prompt.
- Draw a LINE before running the KEEPSTRAIGHT command, and try to select it during the command.
- Press <Enter> at the prompt.
- Press <Escape> at the prompt.
Watch the output to the AutoCAD command line as well as watching the returned values of res.Status for each.
The Document and Application classes you just used are from the Autodesk.AutoCAD ApplicationServices namespace; and the Editor, PromptEntityOptions and PromptEntityResult classes are from the Autodesk.AutoCAD.EditorInput namespace.
This new code first sets the doc variable to point to the Document object representing the drawing document that is currently open and active in AutoCAD (the document you’re editing). The global Application object gives you access to its DocumentManager property, which in turn gives you access to all the documents open in AutoCAD. The DocumentManager has a specific property that returns the MdiActiveDocument. Then we store the editor object for the active document (doc) in the ed variable.
Before telling AutoCAD to prompt the user for input, you have to set some input options. You do this by creating a new PromptEntityOptions object because you will be prompting the user to select an Entity.
You provide the prompt message for AutoCAD to display at the command line - “Select a block reference:” (the vbLF constant simply adds a linefeed to the start of the string – to make sure the prompt starts on a new line).
You specify a message for AutoCAD to display if the user selects the wrong object type (SetRejectMessage).
You tell AutoCAD the user is only allowed to select BlockReference objects (AddAllowedClass). AutoCAD will display the message you specified in SetRejectMessage if the user selects something else.
Now you’ve set up the prompt options, you call the GetEntity method of the editor object, passing in your options. This method returns a PromptEntityResult to tell you which BlockReference the user selected. However, the user may have cancelled the selection by pressing <Escape> or performed a null selection by pressing <Enter>. The If statement immediately exits the ImplementOverrule() method (Exit Sub) if the user didn’t select a BlockReference (i.e. if the Status of the returned PromptEntityResult was anything other than OK).
The AutoCAD API provides a number of Editor.GetXXX methods and corresponding options and result classes to allow you to prompt the user for different types of input:
GetCorner – prompt for a point representing the corner of a rectangle
GetDistance – prompt for a linear distance
GetDouble – prompt for a (double precision) floating point number
GetEntity – prompt for a single entity
GetInteger – prompt for an integer
GetKeyword – prompt for a keyword from a list of keywords
GetPoint – prompt for a point
GetSelection – prompt for a selectionset (more than one entity)
GetString – prompt for a string
GetFileNameForOpen – display the standard AutoCAD file open dialog
GetFileNameForSave – display the standard AutoCAD file save dialog
Now you will use the selection the user made to filter your overrule behavior. You’re going to tell AutoCAD to only apply the overrule to the AttributeReferences of the BlockReference the user selects, instead of its current behavior of overruling every BlockReference in the drawing.
Add the following code (new code in bold, remember):
If res.Status <> PromptStatus.OK Then
Dim objIds() As ObjectId
Dim db As Database = doc.Database
Using trans As Transaction =
' Open the BlockReference for read.
' We know its a BlockReference because we set a filter in
' our PromptEntityOptions
Dim blkRef As BlockReference =
' Record the ObjectIds of all AttributeReferences
' attached to the BlockReference.
Dim attRefColl As AttributeCollection =
ReDim Preserve objIds(attRefColl.Count - 1)
' We only want to create our overrule instance once,
' so we check if it already exists before we create it
' (i.e. this may be the 2nd time we've run the command)
If myOverrule Is Nothing Then
' Specify which Attributes will be overruled
' Make sure overruling is turned on so our overrule works
Overrule.Overruling = True
Time to test your code again:
Remove the breakpoint you set at the start of the ImplementOverrule() method, then press F5. Once AutoCAD is loaded, NETLOAD your plug-in, and open your test drawing.
Run the KEEPSTRAIGHT command and select the rectangular block.
Run the ROTATE command on each of the block inserts in the drawing. You’ll notice that when you’re jigging the rectangular block during the ROTATE command, the attributes rotate with the block until you click in the drawing to set the final rotation. Then all the attributes suddenly jump to being parallel to the WCS x-axis. This is because AutoCAD makes a copy of the block and its attributes when it is being jigged. The copy has a different ObjectId to the original, so the copy isn’t overruled. The block will look something like this when you’ve finished rotating it:
This new code is introducing some new concepts, so let’s go through it carefully.
The first line - Dim objIds() as ObjectId – is a variable declaration just like you’ve seen before, but this time you are declaring an array. An array variable stores a set of objects of the same type. If you know how many objects you want to store in the array when you declare it, you put a number inside the parentheses to specify the number of objects the array will store. Arrays are zero-based, so ‘Dim objIds(9) as ObjectId’ would tell us there will be 10 objects of type ObjectId in this array – at array positions 0, 1, 2, 3, 4, 5, 6, 7, 8, and 9.
When you declare objIds, you don’t yet know how big it needs to be, so you don’t specify a size in the parentheses. You take care of setting the array’s size later in the code using the ReDim keyword.
In the next line, we’re using the Database class for the first time. The Database class represents the entire contents of a DWG file. Every Document object is associated to a Database object (doc.Database property).
Next we see the Using statement for the first time. The Using statement defines the scope of the Transaction variable trans. When the code execution passes the End Using statement, the trans variable goes out of scope and the object it holds will be disposed by the .NET Framework. The Using statement is a good way to ensure objects are cleaned up at exactly the time you want them to be. Without it, cleaning up unneeded objects is left to the .NET Framework ‘garbage collector’, which only runs when the system processor has some spare time available. There are some objects in the AutoCAD API that it is a very bad idea to leave hanging around when you don’t need them, because they only release AutoCAD resources when they are cleaned up (disposed). A Transaction object is one of those.
We’ll explain Transactions in more detail later. For now, you just need to know that you need to use a Transaction like this whenever you want to query or edit objects in the DWG Database. Transactions are started by calling the StartTransaction() method of the Database object (which creates a Transaction object), and they are ended when the Transaction object is disposed (at the End Using statement).
Now you’ve started a Transaction, you use it to retrieve the BlockReference object from the drawing that the user selected. This is done using the Transaction.GetObject() method. The PromptEntityResult object returned from our earlier call to Editor.GetEntity() has an ObjectId property. An ObjectId is a unique identifier for every DBObject in an AutoCAD session. The GetObject() method tells the AutoCAD Database to return the object corresponding to the ObjectId passed in as a parameter. (You know this has to be a BlockReference because we set our PromptEntityOptions to only allow the user to select a BlockReference.) The second parameter of GetObject() tells the Database whether you want to edit the object (OpenMode.ForWrite) or just query its properties without changing them (OpenMode.ForRead). You’re just going to be querying this BlockReference’s properties, so you use OpenMode.ForRead.
Now you have your BlockReference object, you want to find its AttributeReferences (blk.AttributeCollection property). The AttributeCollection property returns an ObjectIdCollection (a collection is a bit like an array). This is where we find out how big our array needs to be. AttributeCollection has a Count property, which you use in the ReDim statement to set the array to the size it needs to be to hold all the ObjectIds in the AttributeCollection.
Once we’ve set the array to the correct size, we use the AttributeReference.CopyTo() method to copy the ObjectIds of all the AttributeReferences in the collection to the array.
Remember we said that arrays were zero-based. Setting the size of the array using attRefColl.Count actually creates an array with one more element than we need. We have to do this because of a quirk in the AttributeCollection.CopyTo method – it requires there to be at least one extra element in the array it’s copying to. The second ReDim statement - ReDim Preserve objIds(attRefColl.Count - 1) - removes that spare array element. The Preserve keyword tells the .NET runtime that it must keep the values that were already set for the elements in the array that aren’t being removed. Without the Preserve keyword, they would all be reset to null ObjectIds.
The last change you made was to call the overrule’s SetIdFilter() method. This is why you had to copy the ObjectIds from the AttributeCollection to the objIds array variable. SetIdFilter() requires an array as a parameter, and not a collection. By calling the SetIdFilter() method of your overrule object (myOverrule.SetIdFilter()), you are telling the overrule to only apply itself to objects that have an ObjectId contained in the array you pass in.
Overrules have five types of filter. You set the filter type you want by calling one of the Overrule SetXXX methods:
- SetIdFilter – Only apply the overrule to objects that have an ObjectId in the array passed to this function
- SetXdataFilter – Only apply the overrule to objects that have Xdata with the specified Registered Application Id.
- SetExtensionDictionaryEntryFilter – Only apply the overrule to objects that have an entry in their extension dictionary with the specified name.
- SetCustomFilter – You additionally override the overrule’s IsApplicable() method to apply your own logical test to decide whether to overrule a particular object. You return True from IsApplicable() if the overrule should be applied.
- SetNoFilter – This is the default if you don’t call one of the other methods – it applies the overrule to every object of the class type you registered the filter for.
You implicitly used SetNoFilter() (by default) in Lesson 1; you’re using SetIdFilter() now; and you’ll use SetXdataFilter() later. Xdata is data you can add to a DBObject to hold information that is used by your application. It’s a type of Extended Entity Data (EED)..(SetXdataFilter() and SetExtensionDictionaryFilter() both use different types of EED. You’ll learn about EED in a later lesson.
In the next lesson you will learn how to save information in a DWG file so your plug-in can read it when the drawing is re-opened.
ObjectIds (and Handles)
It’s very important to be able to uniquely identify every object stored in a DWG Database. Without some kind of unique identifier, the only way to distinguish one object from another would be to compare all their properties. For example, two lines may have different start and end points, but if they don’t you then have to consider if they may be on different layer or have different linetypes. Assigning each object a unique identifier allows you to dispense with such a lengthy and detailed comparison. When you add an object to the DWG Database, it is assigned two unique identifiers – an ObjectID and a Handle.
An ObjectId is unique within an AutoCAD session. No matter how many drawings you open in one session, you’ll never find two objects with the same ObjectId.
ObjectIds are not saved with the drawing. When you close and reopen a drawing, the objects in it will most likely have different ObjectIds. AutoCAD uses ObjectIds to track relationships between objects in a drawing. But when AutoCAD saves these relationships in the DWG file, it translates them to handles.
The AutoCAD .NET API DBObject class – the parent class for all objects that can be stored in the DWG database – exposes an ObjectId property, which is inherited by all the classes that derive from it. When you used the Editor.GetEntity() method in this lesson to ask the user to select a BlockReference, the returned PromptEntityResult object included an ObjectId property that you then used to retrieve that object from the DWG Database.
You won’t use Handles much in your .NET plug-ins. We mention them here because you will likely have seen them when you run the LIST command, or if you’ve dabbled in LISP programming.
A Handle differs from an ObjectID because it is unique within a drawing, but is not unique across all the drawings open in an AutoCAD session. This means that two objects in two different drawings can have the same Handle. (Although combining the Handle with the drawing pathname would be unique.)
Handles are saved with the drawing. When you save and reopen a drawing, each object will have the same Handle as it did the last time. The only time an object’s Handle will change is if you INSERT the object into a new drawing – then the Handles have to change to avoid clashes with the Handles in the drawing you’re inserting into.
The DBObject class exposes a Handle property.
The following table summarizes the similarities differences between Handles and ObjectIds.
Unique in DWG
Unique in session
Saved with DWG
Continuing with our database theme, it shouldn’t come as a surprise that we have a similar mechanism for modifying the DWG Database as for modifying a relational database – we use Transactions.
The concept of a Transaction is simple. Any changes you make within a Transaction are only committed to the Database when the Transaction is committed. If anything goes wrong while you’re modifying the objects in the Database within a Transaction, then you simply abort the Transaction and the Database is returned to the state it was in before the Transaction started. This is shown in the simple flow diagram below.
Transactions are very powerful, because they allow you to easily roll back the effects of a command if the command is cancelled or if something goes wrong. You may have already added 100 Entity objects to a DWG Database inside your custom command when the user decides to cancel the command. Because you added all those Entity objects using a Transaction, you can remove them all by simply aborting the Transaction (instead of having to go back and remove the one-by-one). If the user completes the command instead of cancelling it, you simply commit the Transaction.
Cleaning up (Disposing)
You won’t normally call the Transaction.Abort() method, because a Transaction is automatically aborted if it is disposed without being committed.
What does dispose mean?
Objects in .NET that need to clean up after themselves when you’ve finished with them implement a Dispose() method. Either you or the .NET Framework will call the Dispose() method to tell the object to do that cleanup.
.NET has a concept of ‘garbage collection’. This is when the .NET framework checks through all the objects you’ve created in your program to see if you’re still using them. It assumes that if no variable in your code that is currently in scope is referencing an object, then that object can be destroyed. If the object implements a Dispose() method, the garbage collector will call it before it destroys the object and frees up the memory it was occupying. The garbage collector cleans up objects when there is some free processor time available (during idle time), so it can take a while (sometimes several minutes) for an object to be garbage collected.
The Transaction object implements a Dispose() method. If you don’t correctly dispose of a Transaction object you created before your custom command ends and returns control to AutoCAD, then you will start seeing some very strange AutoCAD behavior - usually culminating in AutoCAD crashing. Because of this, you can’t leave it to the garbage collector – you have to call Dispose() on the Transaction object as soon as you’re finished with it. One way to do that is to explicitly call the Transaction.Dispose() method. But there’s a better way …
The Using…End Using statement you added to your code in this lesson takes care of calling Dispose() for you. The Using statement tells.NET which variable you want to ‘use’:
Using trans As Transaction = db.TransactionManager.StartTransaction
In this case, you’re ‘using’ a Transaction object stored in the variable called trans. The End Using statement tells .NET to immediately cleanup the object you were ‘using’:
Part of that cleanup operation is to call trans.Dispose(). You don’t see that, but that is what is happening behind the scenes. If you were modifying DBObjects using your Transaction, then you’d call the Transaction.Commit() method before you reached the End Using statement to commit the changes you made to the object into the DWG Database. The code for that would look like this:
Using trans As Transaction = db.TransactionManager.StartTransaction
The Transaction.Dispose() method (called when End Using is reached) checks to see if the Transaction.Commit() method has already been called. If it hasn’t, then the Dispose() method automatically calls the Transaction.Abort() method.
If you’re just querying DBObjects in a Transaction and not modifying them, then it doesn’t matter if you commit the Transaction or not. Although committing a Transaction instead of aborting it is slightly faster if you’re opening lots (thousands) of objects in your Transaction.
The bottom line is very simple - remember to always use Using…End Using with Transactions.
Arrays and Collections
is a variable that stores a set of objects of the same type. You saw in this lesson that you can specify the number of objects the array will hold when you declare the array variable:
Dim myArray(9) As Integer
and you can change it later. This code adds an additional 5 elements to the array:
You access an element of an array using the ordinal number. This code is setting the value of the 4th element of myArray to 42.
myArray(3) = 42
Arrays can be multi-dimensional. The following code defines a 10 by 10 two-dimensional array:
Dim myArray(9, 9) As Integer
Think of a two dimension array like a spreadsheet or grid, with the first ordinal being the row number and the second one being the column number. Your array can have any number of dimensions.
An array is simple, fast and efficient, but in some ways the array is a throwback to more primitive programming frameworks to .NET. A more advanced construct in .NET is the collection. At its simplest, you can consider a collection to be an array that grows dynamically as you add and remove elements. You add elements using the Add() method and remove them using the RemoveAt() method. Collections are one-dimensional only. You access an item in the collection using the Item() method.
In this tutorial, you will use AttributeCollection and ObjectIdCollection, which are predefined collections from the AutoCAD .NET API. When you start writing more advanced code, you may create your own collections. Here is some code that adds three ObjectIds to an ObjectIdCollection accesses item 2 in the collection and then removes item 1:
Dim idColl As ObjectIdCollection
Dim objId As ObjectId = idColl.Item(2)
There are several specialized collection type classes in the .NET Framework System.Collections namespace, including:
Dictionary – each object stored in a Dictionary is referenced by a unique key.
SortedDictionary – like a Dictionary, but sorted by the key value.
List – an array whose size varies dynamically.
Queue – a first in first out collection
Stack – a first in last out collection.