LINQ and the AutoCAD .NET API (Part 8)

Creating objects II (DBDictionaries)

This is the eighth 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 8 in our series. Today we want to apply the changes that we made to tables of type SymbolTable to containers of type DBDictionary.

Let's recap how objects are persisted in the AutoCAD database. In general we deal with tables and dictionaries:

Tables Dictionaries
Container Type SymbolTable DBDictionary
Item Type SymbolTableRecord DBDictionaryEntry
IEnumerable Item Type ObjectId DBDictionaryEntry

Each table is derived from SymbolTable, each dictionary form DBDictionary. And each item in a table is derived from SymbolTableRecord, each item in a dictionary is derived from DBDictionaryEntry. Very interesting are the entries we get when we iterate the table or dictionary: when we iterate a SymbolTable, each table entry is the ObjectId of the associated SymbolTableRecord. When we iterate a DBDictionary, each dictionary entry is a DBDictionaryEntry. A DBDictionaryEntry is a key-value-pair, having the ObjectId as the value. We need this detail later for our implementation.

Creating objects II

In last post's listing 6 we created an abstract base class ContainerBase that wraps the access to an AutoCAD table. We will show the listing here again, but we rename the class to TableContainerBase to avoid difficulties to distinguish our classes:


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

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

Listing 1: TableContainerBase

Now we want to apply the structure in this class to dictionaries as well. To do that, we first simply copy the code from the TableContainerBase class, adjust it to DBDictionaries and DBDictionaryEntries and finally put the common parts of the two classes into a common base class.

OK, let's start. We copy and make some changes, such that we can use the class with a DBDictionary:


public abstract class DictionaryContainerBase<T> : IEnumerable<T> where T : DBObject
{
  private readonly Transaction transaction;
  private readonly ObjectId dictionaryId;

  protected DictionaryContainerBase(Transaction transaction, ObjectId dictionaryId)
  {
    this.transaction = transaction;
    this.dictionaryId = dictionaryId;
  }

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

    foreach (DBDictionaryEntry entry in enumerable)
    {
      yield return (T)transaction.GetObject(entry.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 ((DBDictionary)transaction.GetObject(dictionaryId, OpenMode.ForRead)).Contains(name);
  }
  
  public T Create(string name)
  {
    if (!IsValidName(name))
    {
      return null;
    }

    var dict = (T)transaction.GetObject(dictionaryId, OpenMode.ForWrite);

    if (dict.Contains(name))
    {
      return null;
    }

    var newItem = CreateItem();
    dict.SetAt(name, newItem);
    transaction.AddNewlyCreatedDBObject(newItem, true);

    return newItem;
  }

  protected abstract T CreateItem();
}

Listing 2: DictionaryContainerBase

Finding a base class

Our next step is to take out the common parts and put them in a base class called ContainerBase. GetEnumerator is similar but not the same in the two classes. The iterator in DictionaryContainerBase returns items of type DBDictionaryEntry (which, as we said before, have the ObjectId of the actual item in a property named Value), whereas the items of a table iterator are simply the ObjectIds of the BlockTableRecords. As we are only interested in the ObjectId, we could introduce a method GetObjectId(object) that takes the iterator item as an argument and returns the ObjectId:


public abstract class ContainerBase<T> : IEnumerable<T> where T : DBObject
{
  protected readonly Transaction transaction;
  protected readonly ObjectId dictionaryId;

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

  protected abstract ObjectId GetObjectId(object iteratorItem);

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

    foreach (var entry in enumerable)
    {
      yield return (T)transaction.GetObject(GetObjectId(entry), OpenMode.ForRead);
    }
  }

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

Listing 3: ContainerBase

Looks good. We now have our base class and delegated discovering the ObjectId to the derived classes.

The next method to look at is IsValidName(string). I'm not sure if there are different naming constraints for tables and dictionaries, but for the moment we assume that names for table entries and dictionary entries are the same. So we can add IsValidName(string) to the base class, as it is exactly the same in both derived classes.

When we look at Contains(string), we see that there is a difference in the two variants. A SymbolTable has a Has(string) method to check the existence of an item, a DBDictionary has a Contains(string) method to do that. So nothing that we can make reusable in the base class.

Last but not least Create(string). Most of the code is similar, but handling the name of the item is different as well as adding it to the table/dictionary. We could introduce a method named AddItem(string, T) that delegates adding items to the derived classes.

Let's sum up our findings:

  • IsValid(string) comes into the base class
  • Contains(string) is different in both cases and is made abstract in the base class
  • Create(string) has some commonalities, the differences are delegated to the derived classes by introducing AddItem(string, T)

With that in mind, we now extend the class from listing 3:


public abstract class ContainerBase<T> : IEnumerable<T> where T : DBObject
{
  protected readonly Transaction transaction;
  protected readonly ObjectId containerId;

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

  protected abstract ObjectId GetObjectId(object iteratorItem);

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

    foreach (var entry in enumerable)
    {
      yield return (T)transaction.GetObject(GetObjectId(entry), 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 abstract bool Contains(string name);

  protected abstract T CreateNew();

  protected abstract void AddItem(string name, T item);

  public T Create(string name)
  {
    if (!IsValidName(name) || Contains(name))
    {
      return null;
    }

    var newItem = CreateItem();
    AddItem(name, newItem);
    transaction.AddNewlyCreatedDBObject(newItem, true);

    return newItem;
  }
}

Listing 4: ContainerBase

Looks like a nice base class, we can reuse most of the code.

Building the derived classes

Now let's look at the derived classes. What's left for TableContainerBase<T>?


public abstract class TableContainerBase<T> : ContainerBase<T> wehre T : SymbolTableRecord
{
  protected TableContainerBase(Transaction transaction, ObjectId tableId)
    : base(transaction, tableId)
  {
  }
 
  protected override sealed ObjectId GetObjectID(object iteratorItem)
  {
    return (ObjectId)iteratorItem;
  }
 
  public override sealed bool Contains(string name)
  {
    return ((SymbolTable)_transaction.GetObject(_containerId, OpenMode.ForRead)).Has(name);
  }
  
  public override sealed void AddItem((string name, T item)
  {
    item.Name = name;
    ((SymbolTable)_transaction.GetObject(_containerId, OpenMode.ForRead)).Add(newItem);
  }
}

Listing 5: TableContainerBase

TableContainerBase<T> is now reduced to the minimum. One thing to mention is that we use the sealed keyword to prevent the methods from being overridden in derived classes.

Next we have a look at DictionaryContainerBase<T>:


public abstract class DictionaryContainerBase<T> : ContainerBase<T>
{
  protected DictionaryContainerBase(Transaction transaction, ObjectId dictionaryId)
    : base(transaction, dictionaryId)
  {
  }
 
  protected override sealed ObjectId GetObjectID(object iteratorItem)
  {
    return ((DBDictionaryEntry)iteratorItem).Value;
  }
  
  public override sealed bool Contains(string name)
  {
    return ((DBDictionary)_transaction.GetObject(_containerId, OpenMode.ForRead)).Contains(name);
  }
  
  public override sealed void AddItem(string name, T item)
  {
    return (DBDictionary)_transaction.GetObject(_containerId, OpenMode.ForRead)).SetAt(name, item);
  }
}

Listing 6: DictionaryContainerBase

Almost the same as TableContainerBase<T>: reduced to a minimum, only the DBDictionary depended stuff is left.

And how do these changes affect the GeneralHelper class? The first thing we have to change is that all the table container classes (like BlockContainer) now derive from TableContainerBase instead of ContainerBase.

The other thing is that we now have to implement the classes that derive from DictionaryContainerBase. For example the container class for Layouts looks like the this:


public class LayoutContainer : DictionaryContainerBase<Layout>
{
  internal LayoutContainer(Transaction transaction, ObjectId dictionaryId)
    : base(transaction, dictionaryId)
  {
  }

  protected override Layout CreateNew()
  {
    return new Layout();
  }
}

Listing 7: LayoutContainer

And, if we apply this to all dictionaries, we finally have the following GeneralHelper class:


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();
    }
  }
 
  #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
 
  public LayoutContainer Layouts
  {
    get { return new LayoutContainer(transaction, db.LayoutDictionaryId); }
  }
 
  public GroupContainer Groups
  {
    get { return new GroupContainer(transaction, db.GroupDictionaryId); }
  }
 
  public MLeaderStyleContainer MLeaderStyles
  {
    get { return new MLeaderStyleContainer(transaction, db.MLeaderStyleDictionaryId); }
  }
 
  public MlineStyleContainer MlineStyles
  {
    get { return new MlineStyleContainer(transaction, db.MLStyleDictionaryId); }
  }
 
  public MaterialContainer Materials
  {
    get { return new MaterialContainer(transaction, db.MaterialDictionaryId); }
  }
 
  public DBVisualStyleContainer DBVisualStyles
  {
    get { return new DBVisualStyleContainer(transaction, db.VisualStyleDictionaryId); }
  }
 
  public PlotSettingsContainer PlotSettings
  {
    get { return new PlotSettingsContainer(transaction, db.PlotSettingsDictionaryId); }
  }
 
  public TableStyleContainer TableStyles
  {
    get { return new TableStyleContainer(transaction, db.TableStyleDictionaryId); }
  }
 
  public SectionViewStyleContainer SectionViewStyles
  {
    get { return new SectionViewStyleContainer(transaction, db.SectionViewStyleDictionaryId); }
  }
 
  public DetailViewStyleContainer DetailViewStyles
  {
    get { return new DetailViewStyleContainer(transaction, db.DetailViewStyleDictionaryId); }
  }
 
  #endregion
 
  // Handling model/paper space is left out...
}

Listing 8: GeneralHelper

Wrapping up

Finally, the new GeneralHelper class allows us to use the same functionality for dictionaries as well:


[CommandMethod("CreateLayout")]
public static void CreateLayout()
{
  var doc = Application.DocumentManager.MdiActiveDocument;
  var layoutName = GetLayerName();
 
  using (var helper = new GeneralHelper())
  {
    if (helper.Layouts.IsValidName(layoutName))
    {
      doc.Editor.WriteMessage($"{Environment.Newline}'{layoutName}' is not a valid layout name.");
    }
    else if (helper.Layouts.Contains(layoutName))
    {
      doc.Editor.WriteMessage($"{Environment.Newline}Layout '{layoutName}' already exists.");
    }
    else
    {
       helper.Layouts
             .Create(layoutName);
    }
  }
}

Listing 9: CreateLayout

The main improvement to the GeneralHelper class is that we now have a dedicated container class for all the tables and the dictionaries as well. The implementation details are encapsulated in the container classes.

In the next post we further refactor the GeneralHelper class.