This is the first in a series of posts on LINQ an the AutoCAD .NET API. Here’s a complete list of posts in this series.
I recently played around with the AutoCAD .NET API and I want to share some ideas I had on how to make use of IEnumerable<T> when dealing with the drawing database. Generally speaking, the AutoCAD API is very powerful as the drawing is based on a database and you use transactions to interact with the drawing data. If something goes wrong, you simply abort the transaction and your changes are rolled back. Nice. But this comes with the cost of writing a lot of boilerplate code.
As an example, to display the names of all layers you have to do the following (the code is taken from the AutoCAD .NET developer’s guide – I slightly changed it, using a StringBuilder instead of string concatenation):
[CommandMethod("DisplayLayerNames")] public static void DisplayLayerNames() { // Get the current document and database Document acDoc = Application.DocumentManager.MdiActiveDocument; Database acCurDb = acDoc.Database; // Start a transaction using (Transaction acTrans = acCurDb.TransactionManager.StartTransaction()) { // Open the Layer table for read LayerTable acLyrTbl; acLyrTbl = acTrans.GetObject(acCurDb.LayerTableId, OpenMode.ForRead) as LayerTable; var sLayerNames = new StringBuilder(); foreach (ObjectId acObjId in acLyrTbl) { LayerTableRecord acLyrTblRec; acLyrTblRec = acTrans.GetObject(acObjId, OpenMode.ForRead) as LayerTableRecord; sLayerNames.Append("\n" + acLyrTblRec.Name); } Application.ShowAlertDialog("The layers in this drawing are: " + sLayerNames.ToString()); // Dispose of the transaction } }
Well, there’s a lot going on here. This actually means, that there’s a lot of boilerplate code involved which we actually don’t want to deal with. Furthermore one has to have a lot of explicit knowledge about the structure of the API, which may not be obvious to a beginner:
- We have to start a transaction and it has to be disposed of in the end (line 9)
- Layers are stored in a table of type “LayerTable” (line 12)
- The database object has a property “LayerTableID” which is the ID for the table we’re interested in (line 13)
- We get the LayerTable object from the transaction via GetObject and we have to cast it appropriately (line 13 and 14)
- We have to iterate the layer table to get the IDs of the single layers (line 18)
- Layer objects are of type “LayerTableRecord” (line 20)
- We get the LayerTableRecord objects from the transaction via GetObject and we have to cast them appropriately (line 21 and 22)
An Add-In developer’s perspective
From an AutoCAD Add-In developer’s perspective, do we really want to care about all this stuff? In Listing 1, we actually want to somewhow get the layer objects and display their names. So, as we are dealing with a collection of layers, it would be interesting to find a way to use a implementation of IEnumerable<T> to get rid of the transaction and database specific code and “hide” it from the client code.
How can we do that? Let’s start simple: we define a static class called LayerHelper that has one single method called GetLayers, which returns an IEnumerable<LayerTableRecord>:
public static class LayerHelper { public static IEnumerable<LayerTableRecord> GetLayers() { // Not yet implemented... } }
OK, a very simple interface. The signature of GetLayers() already tells us what we get, an enumerable of LayerTableRecords. So we don’t have to deal with IDs, we simply get the layer objects. Now we have to find a way to return all layers in the drawing database. We already have this code in Listing 1. So, to start simple, let’s copying and pasting the example code into our GetLayer method:
public static class LayerHelper { public static IEnumerable<LayerTableRecord> GetLayers() { // Get the current document and database Document acDoc = Application.DocumentManager.MdiActiveDocument; Database acCurDb = acDoc.Database; // Start a transaction using (Transaction acTrans = acCurDb.TransactionManager.StartTransaction()) { // Open the Layer table for read LayerTable acLyrTbl; acLyrTbl = acTrans.GetObject(acCurDb.LayerTableId, OpenMode.ForRead) as LayerTable; foreach (ObjectId acObjId in acLyrTbl) { yield return acTrans.GetObject(acObjId, OpenMode.ForRead) as LayerTableRecord; } } } }
Looks good. The main modification we made is that we removed the part where we collect the layer names and instead yield the LayerTableRecord objects. The transaction handling and the ID stuff is hidden in the GetLayers method. So, if we want to display the layer names like in Listing 1, we can use our helper method like this:
[CommandMethod("DisplayLayerNames1")] public static void DisplayLayerNames1() { var layerNames = new StringBuilder(); foreach (var layer in LayerHelper.GetLayers()) { layerNames.Append("\n" + layer.Name); } Application.ShowAlertDialog("The layers in this drawing are: " + layerNames); }
Looks pretty cool! Our client code now just deals with our buisness logic (collecting the layer names and displaying them). We also have a single entry point, the LayerHelper class, and the GetLayers method gives us what we actually want, all layer object. And the heavy lifting is hidden in the GetLayers method.
Is there a catch?
This is just too cool! But most cool things have a catch, so there is one somewhere, right? Well, yes. There is a catch. The problem is that we must not use AutoCAD objects after the transaction they’ve been created with was disposed of. It’s not a problem in Listing 4, but in general our implementation of GetLayers() is flawed. Let’s look at another example:
[CommandMethod("DisplayLayerNames2")] public static void DisplayLayerNames2() { var layerNames = new StringBuilder(); var list = LayerHelper.GetLayers() .ToList(); // This works, but it's REALLY unsave to access the layer objects here list.ForEach(l => layerNames.Append("\n" + l.Name)); Application.ShowAlertDialog("The layers in this drawing are: " + layerNames); }
This is almost the same as the code in Listing 4, but the problem is in line 6. ToList() yields all layer objects and immediately after that the transaction is disposed of. So in line 9 we’re using objects that are unsafe to access. Getting the Name property in Listing 4 works, but we should not do it. We’ll go into the details of this whole issue in the next post. For now, let us just fix the problem (so we don’t leave a post with code that may not work).
We add a Database and a Transaction parameter to our GetLayers method:
public static class LayerHelper { public static IEnumerable<LayerTableRecord> GetLayers(Database acCurDb, Transaction acTrans) { // Open the Layer table for read LayerTable acLyrTbl; acLyrTbl = acTrans.GetObject(acCurDb.LayerTableId, OpenMode.ForRead) as LayerTable; foreach (ObjectId acObjId in acLyrTbl) { yield return acTrans.GetObject(acObjId, OpenMode.ForRead) as LayerTableRecord; } } }
Unfortunately our client code is now less clean. We still don’t have to deal with the IDs, nor do we have to write code to pull the objects out of the database. But the transaction is back in our client code. On the other hand, the code is still nicer than Listing 1 and we now can safely use ToList():
[CommandMethod("DisplayLayerNames3")] public static void DisplayLayerNames3() { var db = Application.DocumentManager.MdiActiveDocument.Database; using (var tr = db.TransactionManager.StartTransaction()) { var layerNames = new StringBuilder(); var list = LayerHelper.GetLayers(db, tr) .ToList(); // No problem here list.ForEach(l => layerNames.Append("\n" + l.Name)); Application.ShowAlertDialog("The layers in this drawing are: " + layerNames); } }
And we now can use LINQ queries on our layers. Let’s display all layer names that start with a given prefix and sort them alphabetically:
[CommandMethod("DisplayLayerNames4")] public static void DisplayLayerNames4() { var doc = Application.DocumentManager.MdiActiveDocument; var db = doc.Database; var result = doc.Editor.GetString("Enter a prefix:"); if (result.Status == PromptStatus.OK) { using (var tr = db.TransactionManager.StartTransaction()) { var layerNames = new StringBuilder(); LayerHelper.GetLayers(db, tr) .Where(l => l.Name.StartsWith(result.StringResult)) .OrderBy(l => l.Name) .ToList() .ForEach(l => layerNames.Append("\n" + l.Name)); Application.ShowAlertDialog("Layers starting with " + result.StringResult + ": " + layerNames); } } }
In the next post we’ll go into the details of the here described error and we’ll have a look on how to correctly handle transactions and their objects.
This is very good mr Wolfgang thank you. I will definitely be working through your posts over the next week.
Thanks Ben, I’m glad the posts are helpful to you.