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 of AcadDatabase from the active document
  • FromFile which creates an instance of AcadDatabase 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.