This is the third in a series of posts on LINQ an the AutoCAD .NET API. Here’s a complete list of posts in this series.
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 Database db; private Transaction tr; public LayerHelper() { db = Application.DocumentManager.MdiActiveDocument.Database; tr = 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); } } }
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 } }
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); } }
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 amd all table records from SymbolTableRecod. 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); } }
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); } }
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 Database db; private Transaction tr; public GeneralHelper() { db = Application.DocumentManager.MdiActiveDocument.Database; tr = 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); } }
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 Database db; private Transaction tr; public GeneralHelper() { db = Application.DocumentManager.MdiActiveDocument.Database; tr = 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); } } }
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)); } }
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.
There’s still a lot we can do
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.
Hi Mr Wolfgang,
suppose i create a field in a class of type List . Here there is no transaction in scope.
Yet in another method, what if you start a transaction:
List importantBlockReferences = GetBlockReferences();
then later in a method:
using(Transaction tr……)
{
// retrieve block references and open for reading from the list
// we are of course permitted to open the list for reading in a separate transaction – is this correct? was a little confused by your post and Kean’s as well
}
Hi Ben,
transactions can be a difficult thing in AutoCAD .NET programming. I tend to use one single transaction for all operations I perform in a command method.
In the case you described I would probably do something like this:
using(Transaction tr……)
{
// Pass the transaction as an argument to GetBlockReferences and use it
// instead of opening a new one
List importantBlockReferences = GetBlockReferences(tr);
// Do other things using the same transaction
}
Doing everything within the scope of one single transaction is – at least from my point of view – the most secure way to do this.
With this blog series I wanted to desribe how to create a library that encapsulates the transaction stuff such that an addin developer does not have to deal with these things. The library ist still in an early stage of development and can be found on GitHub (https://github.com/wtertinek/Linq2Acad).
-Wolfgang
Ok I will contribute to it and add useful libraries. if it’s tested using Nunit that would be ideal.
Hello Mr Wolfgang: I think line 4 in Listing 5 requires specification of the relevant type: ObjectID? for some reason my compiler refuses to compile it without the type explicitly stated. rgds
Hi Ben, the code in listing 5 works on my computer if I compile the code. Maybe you had a typo and used ObjectID instead of ObjectId?
It’s strange – thx – i managed to make it work anyways. i forgot how.