In this second article of the series, we will look at the ‘AlignmentEntityCollection’ class. This class manages the entities in an ‘Alignment’ object, and you can access it through its ‘Entities’ property. The ‘AlignmentEntityCollection’ class exposes an interface, which allows you, among other things, to add, remove, and access the entities of the alignment, but the interface can be tricky sometimes.
We will start by implementing a simple command, ‘CDS_DISPLAYALIGNMENTENTITIES’, that allows users to select an alignment and display the contained entities. We use the prefix ‘CDS’, which stands for “Civilized Development Sample” to insure our command name does not collide with an existing command. The skeleton of the command is very simple. We prompt the user to select an alignment and we display its entities.
[CommandMethod("CDS_DisplayAlignmentEntities")]
public void CDS_DisplayAlignmentEntities()
{
ObjectId alignmentId = promptForAlignment();
if (ObjectId.Null != alignmentId)
{
displayAlignmentEntities(alignmentId);
}
}
In the command, we are using two method calls that we have not implemented yet: ‘promptForAlignment()’ and ‘displayAlignmentEntities()’, We will implement them now.
private ObjectId promptForAlignment()
{
// We use PromptEntityOptions to insure the entity selected
// is an Alignment object. When filtering for a type of entity,
// we need to set a reject message in case the user selects
// an entity type not allowed.
//
string promptMsg = "\nSelect Alignment: ";
string rejectMsg = "\nSelected entity is not an alignment.";
PromptEntityOptions opts = new PromptEntityOptions(promptMsg);
opts.SetRejectMessage(rejectMsg);
opts.AddAllowedClass(typeof(Alignment), false);
PromptEntityResult result = _editor.GetEntity(opts);
if (result.Status == PromptStatus.OK)
{
return result.ObjectId;
}
return ObjectId.Null;
}
The ‘promptForAlignment()’ method allows the user to select an Alignment object that we will use to display the entities. The method uses the ‘Editor’ object of the current document to prompt for the entity. Access to the ‘Editor’ object has been encapsulated in the private property ‘_editor’, which implementation is shown here.
/// <summary>
/// Returns a reference to the Editor of the current document.
/// </summary>
private Editor _editor
{
get
{
if (m_Editor == null)
{
m_Editor =
Application.DocumentManager.MdiActiveDocument.Editor;
}
return m_Editor;
}
}
private Editor m_Editor = null;
We define an ‘Editor’ member variable to hold a reference to the ‘Editor’ object of the current document. We initialize it to ‘null’, and the reference will be acquired the first time the property is invoked. I like to encapsulate this type of object in a property, so I do not have to write the access code all over my program. It gives it a nicer look, and the code is more readable.
Now, it is the time to implement the ‘displayAlignmentEntities()’ method. We start by creating a transaction. I encapsulated the code to create the transaction in the method ‘startTransaction(). With the transaction created, we open the alignment and display its name. Then we loop through the entities and display their id, class type, entity type, and number of subentities.
private Editor m_Editor = null;private void displayAlignmentEntities(ObjectId alignmentId)
{
using (Transaction tr = startTransaction())
{
Alignment alignment =
alignmentId.GetObject(OpenMode.ForRead) as Alignment;
write("\nAlignment Name: " + alignment.Name);
foreach (AlignmentEntity entity in alignment.Entities)
{
write("\n.. Entity ID: " + entity.EntityId);
write("\n.. Entity Class: " + entity.GetType());
write("\n.. Entity Type: " + entity.EntityType);
write("\n.. Subentities: " + entity.SubEntityCount);
}
}
}
/// <summary>
/// Starts a new database transaction.
/// </summary>
/// <returns>The new transaction object.</returns>
private Transaction startTransaction()
{
Database db = HostApplicationServices.WorkingDatabase;
return db.TransactionManager.StartTransaction();
}
We are ready to run our command. Open in Civil 3D the provided drawing ‘Multiple Alignments.dwg’. In prospector’s center line alignments, you will find an alignment named “SCS+SSC (1)”. Zoom to that alignment and you should see something like the following image.
If you run the command we implemented and select that alignment, you should see the following output in the AutoCAD command line window.
Now, let’s take a look at the alignment geometry in ‘Panorama’. Select the alignment, right-click and select “Edit Alignment Geometry…”. A tool bar will be displayed. The third button from the right “Alignment Grid View” brings the ‘Panorama’ window, which shows the alignment entities.
What’s wrong? From the command output, we see the second entity, which has an ID=2, is shown as an alignment line, but the ‘Panorama’ window shows the second entity as a curve. Furthermore, the ID’s displayed by my command do not seem to match the numbering in ‘Panorama’.
This is due to the fact that the ‘AlignmentEntityCollection’ enumerates the entities ordered by ID. ID’'s are assigned to entities as they are created, and the ID of an entity never changes. When we add an entity an ID is assigned to it. If the entity is then removed, the ID is not reused by the alignment.We cannot assume the alignment entities were created in sequential order from beginning to end; therefore, our command may or may not display the entities sequentially from the beginning of the alignment to its end.
If the sequential order of the entity is important in your program, you cannot enumerate the entities using the default methods. Fortunately, the ‘AlignmentEntityCollection’ provides an interface that allows accessing the entities in sequential order. Let’s create another command (‘CDS_DISPLAYALIGNMENTENTITIESBYORDER’) that allows us to display the entities in sequential order. Most of the code will be the same, so we will refactor our existing code to accommodate the changes and eliminate any duplication.
Looking at our implementation, we see the code that will change is the ‘foreach’ loop that we have in the ‘displayAlignmentEntities()’ method. Let’s extract the code to its own method. This refactoring is called ‘Extract Method’.
private void enumerateEntitiesById(Alignment alignment)
{
foreach (AlignmentEntity entity in alignment.Entities)
{
write("\n.. Entity ID: " + entity.EntityId);
write("\n.. Entity Class: " + entity.GetType());
write("\n.. Entity Type: " + entity.EntityType);
write("\n.. Subentities: " + entity.SubEntityCount);
}
}
We need a way to modify the method we use to enumerate the entities, so let’s define a delegate type and add a delegate to our class.
// To delegate enumeration of entities.
//
private delegate void EnumerateEntitiesDelegate(Alignment alignment);
private EnumerateEntitiesDelegate EnumerateEntities;
Now, we modify ‘displayAlignmentEntities()’ to call the delegate instead of implementing the ‘foreach’ loop itself.
private void displayAlignmentEntities(ObjectId alignmentId)
{
using (Transaction tr = startTransaction())
{
Alignment alignment =
alignmentId.GetObject(OpenMode.ForRead) as Alignment;
write("\nAlignment Name: " + alignment.Name);
EnumerateEntities(alignment);
}
}
We need to implement our new command and modify the existing one to assign the correct method to delegate. Since most of the code is the same for both commands, let’s use ‘Extract Method’ again to refactor and eliminate duplication.
[CommandMethod("CDS_DisplayAlignmentEntities")]
public void CDS_DisplayAlignmentEntities()
{
EnumerateEntities += enumerateEntitiesById;
doDisplayAlignmentEntities();
}
[CommandMethod("CDS_DisplayAlignmentEntitiesByOrder")]
public void CDS_DisplayAlignmentEntitiesByOrder()
{
EnumerateEntities += enumerateEntitiesByOrder;
doDisplayAlignmentEntities();
}
private void doDisplayAlignmentEntities()
{
ObjectId alignmentId = promptForAlignment();
if (ObjectId.Null != alignmentId)
{
displayAlignmentEntities(alignmentId);
}
}
All we need to do now, it is implement the ‘enumerateEntitiesByOrder()’ method that we are assigning to the delegate in our latest command.
private void enumerateEntitiesByOrder(Alignment alignment)
{
AlignmentEntityCollection entities = alignment.Entities;
for (int i = 0; i < entities.Count; i++)
{
AlignmentEntity entity = entities.GetEntityByOrder(i);
write("\n.. Entity Sequence: " + i);
write("\n.. Entity Class: " + entity.GetType());
write("\n.. Entity Type: " + entity.EntityType);
write("\n.. Subentities: " + entity.SubEntityCount);
}
}
We can run the new command using the previous drawing and alignment, and you should see the following output.
As you can see, the entities are now enumerated in the order they appear at the ‘Panorama’ window. Our second entity is an ‘Arc’ indicated as ‘Curve’ in ‘Panorama’, and our sequence goes from 0 to 6 because the ‘AlignmentEntityCollection’ is a zero-based collection. You can show the same sequence number by adding 1 to the index.
There are still some confusing aspects to the output. If you look at the entity at sequence 5, you will see its class type is ‘AlignmentSCS’, which seems to indicate it is a ‘Spiral-Curve-Spiral’ and should contain three sub-entities. But if we look at the entity type, we see it is a ‘Spiral-Curve’, and it only contains two sub-entities. In my next post, I will talk about this discrepancy and how it should be handled in your code. For now, you should download and review the complete sample and test drawing.
P.S. You may be wondering what the ‘write()’ method does. Here is the implementation, which is also provided with the full source.
/// <summary>
/// Writes a message string to the AutocAD command line window.
/// </summary>
/// <param name="msg">String to write.</param>
private void write(string msg)
{
_editor.WriteMessage("\n" + msg);
}
P.P.S. There is some duplication that it is still bothering me. Most of the implementation of ‘enumerateEntitiesById()’ and ‘enumerateEntitiesByOrder()’ is the same. I will leave it as an exercise for you to refactor the code and elimintate the duplication.
P.P.P.S In case you were wondering what pattern we used when we introduced the delegate to enumerate the entities, it is the ‘Strategy Pattern’, which allows to change the behavior of a class by changing the algorithm it uses. Delegates in .NET are an easy way to implement the ‘Strategy Pattern’.
P.P.P.P.S. Apparently, I posted the wrong source file. The link should take you to the correct .zip file now.