LINQ and the AutoCAD .NET API (Part 9)
Brushing up the API
This is the ninth in a series of posts on LINQ an the AutoCAD .NET API. Here's a complete list of posts in this series.
Introduction
This is part 9 of LINQ and the AutoCAD .NET API. Today we want to do some refactoring to get a nicer API.
Naming things is hard*
At the moment our main entry point to our API is the GeneralHelper
class. GeneralHelper
is not really a good name for an API class. The purpose of the GeneralHelper
class is to provide access to the database of the current document, so maybe something like Database
would be a better name. But unfortunately Database
is already the class name of the AutoCAD database object we're dealing with, so to avoid a name conflict, we could, for the sake of simplicity, add a prefix like Acad
. AcadDatabase
, FTW!
First we have a look at how we create instances of AcadDatabase
:
public class AcadDatabase : IDisposable
{
private readonly Database db;
private readonly Transaction tr;
public AcadDatabase()
{
db = Application.DocumentManager.MdiActiveDocument.Database;
tr = db.TransactionManager.StartTransaction();
}
// The rest is left out...
}
Listing 1: AcadDatabase
In the constructor we simply assign the current database to the db
variable and start a new transaction.
Loading a database from file
So right now we're dealing with the database of the active document. But what about reading a DWG file into a new database? The database object provides a method ReadDwgFile(string, FileOpenMode, bool, string)
to open a drawing database from an DWG file. We could incorporate this as a constructor overload into our AcadDatabase
:
public class AcadDatabase : IDisposable
{
private readonly Database db;
private readonly Transaction tr;
public AcadDatabase()
{
db = Application.DocumentManager.MdiActiveDocument.Database;
tr = db.TransactionManager.StartTransaction();
}
public AcadDatabase(string fileName)
: this(fileName, false, null)
{
}
public AcadDatabase(string fileName, bool forWrite)
: this(fileName, forWrite, null)
{
}
public AcadDatabase(string fileName, bool forWrite, string password)
{
if (!File.Exists(fileName))
{
throw new FileNotFoundException();
}
db = new Database(false, true);
db.ReadDwgFile(fileName,
forWrite
? FileOpenMode.OpenForReadAndWriteNoShare
: FileOpenMode.OpenForReadAndAllShare,
false, password);
tr = db.TransactionManager.StartTransaction();
}
// The rest is left out...
}
Listing 2: AcadDatabase
OK, so what did we add? A new constructor with three arguments: the name of the file to open, a bool that indicates whether to open the file for read or write, and a password, if the database is password protected. The other two new constructors are just convenience overloads that set default values for read/write mode and the password.
And voila, now we can handle external databases as well!
[CommandMethod("OpenExternalDwgFile")]
public static void OpenExternalDwgFile()
{
using (var db = new AcadDatabase(@"C:\Files\Drawing1.dwg"))
{
// Do something with the database...
}
}
Listing 3: OpenExternalDwgFile
But there's one thing we can improve: from an API perspective, there's a problem with constructors. As the name of a constructor is the name of the class, constructors don't have a way to indicate via their name what they are actually doing. In contrast, the method we're using for reading in the DWG file, ReadDwgFile
, is perfectly named. The method name clearly indicates the purpose of the method, to read a DWG file (ReadDwgFile
). That's not possible with constructors.
Of course we could carefully name the arguments, so that one could deduce from the arguments what the constructor is doing. But what about a default constructor like What about AcadDatabase()
? A default constructor doesn't have any arguments. In the case of AcadDatabase()
, one can only guess that it is using the database of the active document.
One way around the problem would be, that we add XML documentation that indicates the purpose of the constructor, but we should strive for an API that is in a way self-documenting by the class and method names.
Factory methods
So how can we overcome the problem with constructors? One way to go is to make all constructors private and to introduce static factory methods, which create and return the objects.
For example, instead of using our newly introduced constructor that loads a database form a DWG file, we could convert it into a static method that has a more meaningful name, something like FromFile
:
public class AcadDatabase : IDisposable
{
private readonly Database db;
private readonly Transaction tr;
public AcadDatabase()
{
db = Application.DocumentManager.MdiActiveDocument.Database;
tr = db.TransactionManager.StartTransaction();
}
public static AcadDatabase FromFile(string fileName)
{
return FromFile(fileName, false, null);
}
public static AcadDatabase FromFile(string fileName, bool forWrite)
{
return FromFile(fileName, forWrite, null);
}
public static AcadDatabase FromFile(string fileName, bool forWrite, string password)
{
if (!File.Exists(fileName))
{
throw new FileNotFoundException();
}
var acadDb = new AcadDatabase();
acadDb.db = new Database(false, true);
acadDb.db.ReadDwgFile(fileName,
forWrite
? FileOpenMode.OpenForReadAndWriteNoShare
: FileOpenMode.OpenForReadAndAllShare,
false, password);
acadDb.tr = db.TransactionManager.StartTransaction();
retuurn acadDb;
}
// The rest is left out...
}
Listing 4: AcadDatabase
We now can open an external file using the following command:
[CommandMethod("OpenExternalDwgFile")]
public static void OpenExternalDwgFile()
{
using (var db = AcadDatabase.FromFile(@"C:\Files\Drawing1.dwg"))
{
// Do something with the database...
}
}
Listing 5: OpenExternalDwgFile
This may seem to be just a minor change, but using factory methods we now have a way to give the default constructor a more expressive name, like FromActiveDocument
:
public class AcadDatabase : IDisposable
{
private readonly Database db;
private readonly Transaction tr;
private AcadDatabase(Database db)
{
this.db = db;
this.tr = db.TransactionManager.StartTransaction();
}
public static AcadDatabase FromActiveDocument()
{
return AcadDatabase(Application.DocumentManager.MdiActiveDocument.Database);
}
public static AcadDatabase FromFile(string fileName)
{
return FromFile(fileName, false, null);
}
public static AcadDatabase FromFile(string fileName, bool forWrite)
{
return FromFile(fileName, forWrite, null);
}
public static AcadDatabase FromFile(string fileName, bool forWrite, string password)
{
if (!File.Exists(fileName))
{
throw new FileNotFoundException();
}
var db = new Database(false, true);
db.ReadDwgFile(fileName,
forWrite
? FileOpenMode.OpenForReadAndWriteNoShare
: FileOpenMode.OpenForReadAndAllShare,
false, password);
return new AcadDatabase(db);
}
// The rest is left out...
}
Listing 6: AcadDatabase
After this refactoring, there's only one constructor left, which is private and only called from the factory methods. And the factory methods are:
FromActiveDocument
which, as the name implies (and that's what we want), creates an instance ofAcadDatabase
from the active documentFromFile
which creates an instance ofAcadDatabase
from a given DWG file
Importing a block
Importing a block into the active database is a common use of loading a drawing database from a file. We could add an Import(BlockTableRecord)
method to our BlockContainer
that encapsulates the import action:
public IdMapping ImportBlock(BlockTableRecord block)
{
// In the current implementation we don't have the db field,
// we would have to pass in the database via the constructor
using (var idCollection = new ObjectIdCollection(new[] { block.ObjectId }))
using (var mapping = new IdMapping())
{
db.WblockCloneObjects(idCollection, db.BlockTableId, mapping,
DuplicateRecordCloning.Ignore, false);
return mapping;
}
}
Listing 7: ImportBlock
Finally we have a pretty expressive way of importing a block from a drawing file into the active drawing:
[CommandMethod("ImportMyBlock")]
public static void ImportMyBlock()
{
using (var activeDb = AcadDatabase.FromActiveDocument())
using (var blockDb = AcadDatabase.FromFile(@"C:\Files\Drawing1.dwg"))
{
// For demo purpose we assume that there is a block named MyBlock
// and we don't do any error checking
var block = blockDb.Blocks
.First(b => b.Name == "MyBlock");
activeDb.Blocks
.Import(block);
}
}
Listing 8: ImportMyBlock
Except for having the import functionality encapsulated in a single method, the nice thing is that Import
makes our client code semantically more intuitive: The block container has a method named Import that takes the actual block object we want to import as an argument.
What's next
And that's it for today's post. In the next post we optimize our code.