LINQ and the AutoCAD .NET API (Part 7)

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.

Welcome to part 7 of LINQ and the AutoCAD .NET API. It’s been a while since my last post. From now on I try to write new posts on this series every two weeks, but don’t pin me down on this…^^

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.

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 static void CreateLayer()
{
  var layerName = GetLayerName();
  var doc = Application.DocumentManager.MdiActiveDocument;

  try
  {
    SymbolUtilityServices.ValidateSymbolName(layerName, false);
  }
  catch
  {
    doc.Editor.WriteMessage("\n\"" + 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("\nLayer \"" + layerName + "\" already exists.");
      return;
    }

    var ltr = new LayerTableRecord()
              {
                Name = layerName
              };

    lt.Add(ltr);
    tr.AddNewlyCreatedDBObject(ltr, true);

    tr.Commit();
  }
}

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 static 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);
    }
  }
}

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 Database db;
  private 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
}

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 Transaction _transaction;
  private ObjectId _layerTableID;

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

  public IEnumerator<LayerTableRecord> GetEnumerator()
  {
    var enumerable = (IEnumerable)_transaction.GetObject(_layerTableID, 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 class GeneralHelper : IDisposable
{
  private Database db;
  private 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); }
  }

   // ...
}

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 Transaction _transaction;
  private 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);
  }
}

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

Refactoring

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> wehre T : SymbolTableRecord
{
  private Transaction _transaction;
  private ObjectId _tableID;

  protected ContainerBase(Transaction transaction, ObjectId tableID)
  {
    _transaction = transaction;
    _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 = (T)_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();
}

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();
  }
}

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();
  }
}

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

public class GeneralHelper : IDisposable
{
  private Database db;
  private 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
}

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. See ya!

Advertisements

6 thoughts on “LINQ and the AutoCAD .NET API (Part 7)

  1. Is there a way to translate this command to vb.net?
    public abstract class ContainerBase : IEnumerable wehre T

    • Hi Edson,

      the full code of the project is available on GitHub: https://github.com/wtertinek/Linq2Acad

      So it should be possible to reference the code from a VB.NET project. To set up the project references follow the instructions in the README (section “Referencing AutoCAD assemblies”).

      Please be aware that the code is just a prototype at the moment and not production ready.

      -Wolfgang

      • Mr Wolfgang,

        A question: somethings like Count() have to be replimented because you are using a wrapper container. if you weren’t using a wrapper container then there would be no need to rewrite Count() and other Linq extension methods? am i understanding things correctly?

        In other words, why not have the create SymbolTableRecords be implemented separately without the need for a container wrapper?

        with kind regards

        Ben

  2. Hi Ben,

    you’re right, one does not need a container wrapper to get these things done. Writing a container class was appropriate in my case, but creating a static method in a separate class would work as well.

    Wolfgang

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s