LINQ and the AutoCAD .NET API (Part 1)

Motivation

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.

Introduction

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. This is nice, but comes with the cost of writing a lot of boilerplate code.

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


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

    string sLayerNames = "";

    foreach (ObjectId acObjId in acLyrTbl)
    {
      LayerTableRecord acLyrTblRec;
      acLyrTblRec = acTrans.GetObject(acObjId, OpenMode.ForRead) as LayerTableRecord;

      sLayerNames = sLayerNames + "\n" + acLyrTblRec.Name;
    }

    Application.ShowAlertDialog("The layers in this drawing are: " + sLayerNames);

    // Dispose of the transaction
  }
}

Listing 1: DisplayLayerNames

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:

  1. We have to start a transaction and it has to be disposed of in the end (line 9)
  2. Layers are stored in a table of type LayerTable (line 12)
  3. The database object has a property LayerTableID which is the ID for the table we're interested in (line 13)
  4. We get the LayerTable object from the transaction via GetObject and we have to cast it appropriately (line 13)
  5. We have to iterate the layer table to get the IDs of the single layers (line 17)
  6. Layer objects are of type LayerTableRecord (line 19)
  7. We get the LayerTableRecord objects from the transaction via GetObject and we have to cast them appropriately (line 20)

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 an 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...
  }
}

Listing 2: LayerHelper

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

Listing 3: LayerHelper implemented

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 (we modify the initial code and use a StringBuilder and string interpolation instead of string concatenation for the remaining code samples):


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

Listing 4: DisplayLayerNames1

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 objects. And the heavy lifting is hidden in the GetLayers method.


The problem

A very nice implementation, 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
  foreach (var layer in list)
  {
    layerNames.Append($"\n{layer.Name}");
  }

  Application.ShowAlertDialog($"The layers in this drawing are: {layerNames}");
}

Listing 5: DisplayLayerNames2 

This is almost the same as the code in listing 4, but the problem is in line 5. 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).

And a fix

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

Listing 6: LayerHelper

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
    foreach (var layer in list)
    {
      layerNames.Append($"\n{layer.Name}");
    }

    Application.ShowAlertDialog($"The layers in this drawing are: {layerNames}");
  }
}

Listing 7: DisplayLayerNames3

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

      foreach (var layer in LayerHelper.GetLayers(db, tr)
                                       .Where(l => l.Name.StartsWith(result.StringResult))
                                       .OrderBy(l => l.Name))
      {
        layerNames.Append($"\n{layer.Name}");
      }

      Application.ShowAlertDialog($"Layers starting with {result.StringResult}: {layerNames}");
    }
  }
}

Listing 8: DisplayLayerNames4

What's next

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.