LINQ and the AutoCAD .NET API (Part 3)

From a LayerTable to SymbolTables

This is the third in a series of posts on LINQ and the AutoCAD .NET API. Here's a complete list of posts in this series.

Introduction

This is part three of a series of posts on LINQ and the AutoCAD .NET API. In part 2 we stopped with the following implementation of our LayerHelper:


public class LayerHelper : IDisposable
{
  private readonly Database db;
  private readonly Transaction tr;

  public LayerHelper()
    : this (Application.DocumentManager.MdiActiveDocument.Database,
            db.TransactionManager.StartTransaction())
  {
  }

  public LayerHelper(Database db, Transaction tr)
  {
    this.db = db;
    this.tr = tr;
  }

  public void Dispose()
  {
    if (tr != null && !tr.IsDisposed)
    {
      tr.Dispose();
    }
  }

  public IEnumerable<LayerTableRecord> GetLayers()
  {
    var table = (LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead);

    foreach (ObjectId id in table)
    {
      yield return (LayerTableRecord)tr.GetObject(id, OpenMode.ForRead);
    }
  }
}

Listing 1: LayerHelper

Generalization

The idea was to make the LayerHelper more general, to not only provide access to layers, but to other tables as well:


[CommandMethod("MyCommand")]
public static void MyCommand()
{
  using (var helper = new GeneralHelper())
  {
    var layers = helper.GetLayers();
    var blocks = helper.GetBlocks();
    var viewports = helper.GetViewports();
    // And so on
  }
}

Listing 2: MyCommand

Right now we only have GetLayers() implemented. But it's rather easy to implement - as an example - GetBlocks() on the GeneralHelper, because the code to get all blocks from the database is actually the same like for layers:


public IEnumerable<BlockTableRecord> GetBlocks()
{
  var table = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);

  foreach (ObjectId id in table)
  {
    yield return (BlockTableRecord)tr.GetObject(id, OpenMode.ForRead);
  }
}

Listing 3: GetBlocks

Tables

If we compare the GetLayers() and GetBlocks() methods, we can easily see that dealing with tables has the following pattern:

  • The table that contains all records is of type {Item}Table
  • Each item in the table is of type {Item}TableRecord
  • The ID for the table object can be obtained from a property of the database object called {Item}TableId

And {Item} is (according to the ObjectARX Managed Class Reference Guide) one of the following names: Block, DimStyle, Layer, Linetype, RegApp, TextStyle, Ucs, Viewport, View. Put together we get the following:

Table type Record type ID property
BlockTable BlockTableRecord BlockTableId
DimStyleTable DimStyleTableRecord DimStyleTableId
LayerTable LayerTableRecord LayerTableId
LinetypeTable LinetypeTableRecord LinetypeTableId
RegAppTable RegAppTableRecord RegAppTableId
TextStyleTable TextStyleTableRecord TextStyleTableId
UcsTable UcsTableRecord UcsTableId
ViewportTable ViewportTableRecord ViewportTableId
ViewTable ViewTableRecord ViewTableId

It's important to notice that all tables are derived from SymbolTable and all table records from SymbolTableRecord. Therefore we can easily turn GetLayers() into a generic GetItems() method, that takes the table ID as an argument:


private IEnumerable<TItem> GetItems<TTable, TItem>(ObjectId tableID)
    where TTable : SymbolTable
    where TItem : SymbolTableRecord
{
  var table = (TTable)tr.GetObject(tableID, OpenMode.ForRead);

  foreach (ObjectId id in table)
  {
    yield return (TItem)tr.GetObject(id, OpenMode.ForRead);
  }
}

Listing 4: GetItems

Looks good! But there's still room for optimization: the only thing we do with the table is that we iterate the IDs. So there has to be some enumerable interface in play. A closer look into the ObjectARX Managed Class Reference Guide tells us, that the base class SymbolTable implements IEnumerable. So it should be OK to simply cast the table object we get from the transaction to IEnumerable. Hence we can remove the generic parameter for the table. This makes the method much simpler:


private IEnumerable<T> GetItems<T>(ObjectId tableID) where T : SymbolTableRecord
{
  var table = (IEnumerable)tr.GetObject(tableID, OpenMode.ForRead);

  foreach (ObjectId id in table)
  {
    yield return (T)tr.GetObject(id, OpenMode.ForRead);
  }
}

Listing 5: GetItems

Alright! Now let's put everything together: at first we rename the LayerHelper to GeneralHelper. Then we add the generic GetItems() method and inflate the class a bit by adding one "wrapper" method for each table type:


public class GeneralHelper : IDisposable
{
  private readonly Database db;
  private readonly Transaction tr;

  public GeneralHelper()
    : this(Application.DocumentManager.MdiActiveDocument.Database,
           db.TransactionManager.StartTransaction())
  {
  }

  public GeneralHelper(Database db, Transaction tr)
  {
    this.db = db;
    this.tr = tr;
  }

  public void Dispose()
  {
    if (tr != null && !tr.IsDisposed)
    {
      tr.Dispose();
    }
  }

  private IEnumerable<T> GetItems<T>(ObjectId tableID) where T : SymbolTableRecord
  {
    var table = (IEnumerable)tr.GetObject(tableID, OpenMode.ForRead);

    foreach (ObjectId id in table)
    {
      yield return (T)tr.GetObject(id, OpenMode.ForRead);
    }
  }

  public IEnumerable<BlockTableRecord> GetBlocks()
  {
    return GetItems<BlockTableRecord>(db.BlockTableId);
  }

  public IEnumerable<DimStyleTableRecord> GetDimStyles()
  {
    return GetItems<DimStyleTableRecord>(db.DimStyleTableId);
  }

  public IEnumerable<LayerTableRecord> GetLayers()
  {
    return GetItems<LayerTableRecord>(db.LayerTableId);
  }

  public IEnumerable<LinetypeTableRecord> GetLinetypes()
  {
    return GetItems<LinetypeTableRecord>(db.LinetypeTableId);
  }

  public IEnumerable<RegAppTableRecord> GetRegApps()
  {
    return GetItems<RegAppTableRecord>(db.RegAppTableId);
  }

  public IEnumerable<TextStyleTableRecord> GetTextStyles()
  {
    return GetItems<TextStyleTableRecord>(db.TextStyleTableId);
  }

  public IEnumerable<UcsTableRecord> GetUcss()
  {
    return GetItems<UcsTableRecord>(db.UcsTableId);
  }

  public IEnumerable<ViewportTableRecord> GetViewports()
  {
    return GetItems<ViewportTableRecord>(db.ViewportTableId);
  }

  public IEnumerable<ViewTableRecord> GetViews()
  {
    return GetItems<ViewTableRecord>(db.ViewTableId);
  }
}

Listing 6: GeneralHelper

Voila! We now have a GeneralHelper that fully supports what we intended in the client code in listing 2. Still another small optimization can be done: if we change the public Get methods to properties, we get a more intuitive way to access the items:


public class GeneralHelper : IDisposable
{
  private readonly Database db;
  private readonly Transaction tr;

  public GeneralHelper()
    : this(Application.DocumentManager.MdiActiveDocument.Database,
           db.TransactionManager.StartTransaction())
  {
  }

  public GeneralHelper(Database db, Transaction tr)
  {
    this.db = db;
    this.tr = tr;
  }

  public void Dispose()
  {
    if (tr != null && !tr.IsDisposed)
    {
      tr.Dispose();
    }
  }

  private IEnumerable<T> GetItems<T>(ObjectId tableID) where T : SymbolTableRecord
  {
    var table = (IEnumerable)tr.GetObject(tableID, OpenMode.ForRead);

    foreach (ObjectId id in table)
    {
      yield return (T)tr.GetObject(id, OpenMode.ForRead);
    }
  }

  public IEnumerable<BlockTableRecord> Blocks
  {
    get { return GetItems<BlockTableRecord>(db.BlockTableId); }
  }

  public IEnumerable<DimStyleTableRecord> DimStyles
  {
    get { return GetItems<DimStyleTableRecord>(db.DimStyleTableId); }
  }

  public IEnumerable<LayerTableRecord> Layers
  {
    get { return GetItems<LayerTableRecord>(db.LayerTableId); }
  }

  public IEnumerable<LinetypeTableRecord> Linetypes
  {
    get { return GetItems<LinetypeTableRecord>(db.LinetypeTableId); }
  }

  public IEnumerable<RegAppTableRecord> RegApps
  {
    get { return GetItems<RegAppTableRecord>(db.RegAppTableId); }
  }

  public IEnumerable<TextStyleTableRecord> TextStyles
  {
    get { return GetItems<TextStyleTableRecord>(db.TextStyleTableId); }
  }

  public IEnumerable<UcsTableRecord> Ucss
  {
    get { return GetItems<UcsTableRecord>(db.UcsTableId); }
  }

  public IEnumerable<ViewportTableRecord> Viewports
  {
    get { return GetItems<ViewportTableRecord>(db.ViewportTableId); }
  }

  public IEnumerable<ViewTableRecord> Views
  {
    get { return GetItems<ViewTableRecord>(db.ViewTableId); }
  }
}

Listing 7: GeneralHelper

Bye-bye boilerplate

And now we can unleash the real beauty and power of LINQ! Let's write a command that displays all layer names that have the name prefix "ABC_":


[CommandMethod("DisplayAbcLayers")]
public static void DisplayAbcLayers()
{
  using (var helper = new GeneralHelper())
  {
    var names = from layer in helper.Layers
                where layer.Name.StartsWith("ABC_")
                select layer.Name;

    Application.ShowAlertDialog("ABC_* layers:" + Environment.NewLine + string.Join(Environment.NewLine, names));
  }
}

Listing 8: DisplayAbcLayers

Awesome. The expressiveness of a query like the one in line 6 is just unbeatable! And no boilerplate code involved! The only "setup" we do is to create the helper inside a using statement. And what follows is just business logic. Yeah!

Admittedly, this example is trivial, but still much easier to read and comprehend than the one we started from in part 1.


But there's more

We've come pretty far, but there's still a lot we can do. By now we've covered all tables in the AutoCAD drawing database, but many other interesting items are stored in another main category of AutoCAD containers: dictionaries. In the next post we'll extend the GeneralHelper with dictionary accessors and do some further refactorings.