LINQ and the AutoCAD .NET API (Part 7)

Creating objects I (SymbolTables)

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

Introduction

Welcome to part 7 of LINQ and the AutoCAD .NET API. In the last post we extended the GeneralHelper class to support write operations. In today's post we have a look on how we can add a convenient way to create new AutoCAD objects and add them to the database. And be prepared, there's a lot of code in this post.

Object creation

Let's start with an example. The standard procedure to create a new layer using the .NET API goes something like this:


[CommandMethod("CreateLayer")]
private void CreateLayer()
{
  var layerName = GetLayerName();
  var doc = Application.DocumentManager.MdiActiveDocument;

  try
  {
    SymbolUtilityServices.ValidateSymbolName(layerName, false);
  }
  catch
  {
    doc.Editor.WriteMessage($"{Environment.NewLine}'{layerName}' is not a valid layer name");
    return;
  }

  using (var tr = doc.Database.TransactionManager.StartTransaction())
  {
    var lt = (LayerTable)tr.GetObject(doc.Database.LayerTableId, OpenMode.ForWrite);
    
    if (lt.Has(layerName))
    {
      doc.Editor.WriteMessage($"{Environment.NewLine}Layer '{layerName}' already exists.");
      return;
    }
    
    var ltr = new LayerTableRecord()
              {
                Name = layerName
              };
    
    lt.Add(ltr);
    tr.AddNewlyCreatedDBObject(ltr, true);
    
    tr.Commit();
  }
}

Listing 1: CreateLayer

This is pretty much what we can find in most of the AutoCAD .NET tutorials out there. In this blog post series our overall goal is to create an API that simplifies working with the AutoCAD API. Now, can we find a neat way to integrate object creation into our GeneralHelper? What could the call to the GeneralHelper class look like? Something like this would be nice:


[CommandMethod("CreateLayer")]
public void CreateLayer()
{
  var doc = Application.DocumentManager.MdiActiveDocument;
  var layerName = GetLayerName();

  using (var helper = new GeneralHelper())
  {
    if (!helper.Layers.IsValidName(layerName))
    {
      doc.Editor.WriteMessage("\n\"" + layerName + "\" is not a valid layer name.");
    }
    else if (helper.Layers.Contains(layerName))
    {
      doc.Editor.WriteMessage("\nLayer \"" + layerName + "\" already exists.");
    }
    else
    {
      helper.Layers.Create(layerName);
    }
  }
}

Listing 2: CreateLayer

This would be a more intuitive API: the helper object as our entry point (we should find a better name for the GeneralHelper btw), the property Layers to access all the layer dependent stuff, two methods to validate the layer name and a Create method to actually create the new layer.

Let's go back and look at the current implementation of the GeneralHelper. We only look at the parts we need for the layers, the rest is cut out:


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

  public GeneralHelper()
  {
    db = Application.DocumentManager.MdiActiveDocument.Database;
    tr = db.TransactionManager.StartTransaction();
  }

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

  private IEnumerable<T> GetTableItems<T>(ObjectId tableID) where T : SymbolTableRecord
  {
    if (tableID.IsValid)
    {
      var table = (IEnumerable)tr.GetObject(tableID, OpenMode.ForRead);
  
      foreach (ObjectId id in table)
      {
        yield return (T)tr.GetObject(id, OpenMode.ForRead);
      }
    }
    else
    {
      yield break;
    }
  }

  // ...

  #region Tables

  // ...

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

  // ...

  #endregion
}

Listing 3: GeneralHelper

So IEnumerable<LayerTableRecord> is the type of our Layers property. Since we want to have the Create method as a member of the Layers property's type, we could create a class called - say - LayerContainer that implements IEnumerable<LayerTableRecord>. Let's do this:


public class LayerContainer : IEnumerable<LayerTableRecord>
{
  private readonly Transaction transaction;
  private readonly ObjectId layerTableID;

  internal LayerContainer(Transaction transaction, ObjectId layerTableID)
  {
    this.transaction = transaction;
    this.layerTableID = layerTableID;
  }

  public IEnumerator<LayerTableRecord> GetEnumerator()
  {
    var enumerable = (IEnumerable)_transaction.GetObject(_layerTableID, OpenMode.ForRead);
    
    foreach (ObjectId objectID in enumerable)
    {
      yield return (LayerTableRecord)_transaction.GetObject(objectID, OpenMode.ForRead);
    }
  }

  System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
  {
    return GetEnumerator();
  }
}

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

  public GeneralHelper()
  {
    db = Application.DocumentManager.MdiActiveDocument.Database;
    tr = db.TransactionManager.StartTransaction();
  }

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

  // ...

  public LayerContainer Layers
  {
    get { return new LayerContainer(tr, db.LayerTableId); }
  }

  // ...
}

Listing 4: LayerContainer and GeneralHelper

OK, two new things here. We have the LayerContainer class that implements IEnumerable<LayerTableRecord> and we changed the type of the Layers property to LayerContainer (the unnecessary parts in the GeneralHelper class are again left out).

We didn't break anything with this change, everything works like before. And now we can easily add the Create method and the validation methods to the LayerContainer class:


public class LayerContainer : IEnumerable<LayerTableRecord>
{
  private readonly Transaction transaction;
  private readonly ObjectId layerTableID;

  internal LayerContainer(Transaction transaction, ObjectId layerTableID)
  {
    transaction = transaction;
    layerTableID = layerTableID;
  }

  // ...

  public bool IsValidName(string layerName)
  {
    try
    {
      SymbolUtilityServices.ValidateSymbolName(layerName, false);
      return true;
    }
    catch
    {
      return false;
    }
  }

  public bool Contains(string layerName)
  {
    return ((LayerTable)transaction.GetObject(layerTableID, OpenMode.ForRead)).Has(layerName);
  }
  
  public LayerTableRecord Create(string layerName)
  {
    if (!IsValidName(layerName))
    {
      return null;
    }
    
    var lt = (LayerTable)transaction.GetObject(layerTableID, OpenMode.ForWrite);
    
    if (lt.Has(layerName))
    {
      return null;
    }
    
    var ltr = new LayerTableRecord() { Name = layerName };
    
    lt.Add(ltr);
    transaction.AddNewlyCreatedDBObject(ltr, true);
  }
}

Listing 5: LayerContainer

Looks good! Now the code in listing 2 works as expected.


Let's refactor

It's time for some refactoring. Is the functionality in the LayerContainer class applicable to other AutoCAD classes? Can we find an abstract base class that encapsualtes the common behaviour?

Let's see...

LayerTable is derived from SymbolTable, LayerTableRecord form SymbolTableRecord. It seems that all tables are derved from SymbolTable and the table records are derived from SymbolTableRecord. So we could make an abstract base class that represents a SymbolTable and works with any class that is derived from SymbolTableRecord. And we could pass the actual derived type as a type parameter to work with the "real" SymbolTableRecord type. Let's give it a try:


public abstract class ContainerBase<T> : IEnumerable<T> where T : SymbolTableRecord
{
  private readonly Transaction transaction;
  private readonly ObjectId tableID;

  protected ContainerBase(Transaction transaction, ObjectId tableID)
  {
    this.transaction = transaction;
    this.tableID = tableID;
  }

  public IEnumerator<T> GetEnumerator()
  {
    var enumerable = (IEnumerable)transaction.GetObject(_tableID, OpenMode.ForRead);

    foreach (ObjectId objectID in enumerable)
    {
      yield return (T)transaction.GetObject(objectID, OpenMode.ForRead);
    }
  }

  System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
  {
    return GetEnumerator();
  }

  public bool IsValidName(string name)
  {
    try
    {
      SymbolUtilityServices.ValidateSymbolName(name, false);
      return true;
    }
    catch
    {
      return false;
    }
  }

  public bool Contains(string name)
  {
    return ((SymbolTable)transaction.GetObject(_tableID, OpenMode.ForRead)).Has(name);
  }
  
  public T Create(string name)
  {
    if (!IsValidName(name))
    {
      return null;
    }

    var table = (SymbolTable)transaction.GetObject(_tableID, OpenMode.ForWrite);

    if (table.Has(name))
    {
      return null;
    }

    var newItem = CreateItem();
    newItem.Name = name;

    table.Add(newItem);
    transaction.AddNewlyCreatedDBObject(newItem, true);

    return newItem;
  }

  protected abstract T CreateItem();
}

Listing 6: ContainerBase

We replaced everything that was specific to LayerTableRecord stuff with the generic type T. Creating a new item has been moved to a protected abstract method called CreateItem that returns the new item. Hence the creation of a new item is handled by the derived class.

So, our LayerContainer has been shrunk to only a few lines of code. We just call the base class constructor and we implement the CreateNew method, and that's it:


public class LayerContainer : ContainerBase<LayerTableRecord>
{
  internal LayerContainer(Transaction transaction, ObjectId containerID)
    : base(transaction, containerID)
  {
  }

  protected override LayerTableRecord CreateItem()
  {
    return new LayerTableRecord();
  }
}

Listing 7: LayerContainer

The same pattern is now applicable to all classes that are derived from SymbolTableLayer, like BlockTableRecord for instance:


public class BlockContainer : ContainerBase<BlockTableRecord>
{
  internal BlockContainer(Transaction transaction, ObjectId containerID)
    : base(transaction, containerID)
  {
  }

  protected override BlockTableRecord CreateItem()
  {
    return new BlockTableRecord();
  }
}

Listing 8: BlockContainer

If we do this for all classes that are derived from SymbolTableLayer, our GeneralHelper now looks like this:


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

  public GeneralHelper()
  {
    db = Application.DocumentManager.MdiActiveDocument.Database;
    tr = db.TransactionManager.StartTransaction();
  }

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

  private IEnumerable<T> GetDictItems<T>(ObjectId dictID) where T : DBObject
  {
    if (dictID.IsValid)
    {
      var dict = (IEnumerable)tr.GetObject(dictID, OpenMode.ForRead);

      foreach (DBDictionaryEntry entry in dict)
      {
        yield return (T)tr.GetObject((ObjectId)entry.Value, OpenMode.ForRead);
      }
    }
    else
    {
      yield break;
    }
  }

  #region Tables

  public BlockContainer Blocks
  {
    get { return new BlockContainer(tr, db.BlockTableId); }
  }

  public LayerContainer Layers
  {
    get { return new LayerContainer(tr, db.LayerTableId); }
  }

  public DimStyleContainer DimStyles
  {
    get { return new DimStyleContainer(tr, db.DimStyleTableId); }
  }

  public LinetypeContainer Linetypes
  {
    get { return new LinetypeContainer(tr, db.LinetypeTableId); }
  }

  public RegAppContainer RegApps
  {
    get { return new RegAppContainer(tr, db.RegAppTableId); }
  }

  public TextStyleContainer TextStyles
  {
    get { return new TextStyleContainer(tr, db.TextStyleTableId); }
  }

  public UcsContainer Ucss
  {
    get { return new UcsContainer(tr, db.UcsTableId); }
  }

  public ViewportContainer Viewports
  {
    get { return new ViewportContainer(tr, db.ViewportTableId); }
  }

  public ViewContainer Views
  {
    get { return new ViewContainer(tr, db.ViewTableId); }
  }

  #endregion

  #region Dictionaries

  // ... Nothing changed here...

  #endregion
}

Listing 9: GeneralHelper


Wrapping up

How did we improve the GeneralHelper class? We found a nice way to hide the complexity of creating new AutoCAD objects behind our API.

Behind the scenes we were able to make the creation code reusable by putting it into a base class, so the only thing that has to be implemented by a derived class is creating and returning a new object in the CreateItem method. The validation code is the same for all implementers, so it is implemented in the base class.

What is still left is to further refactor the GeneralHelper class to use the creation code not only for tables, but for dictionaries as well. So in the next post we'll do some refactoring.