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.