LINQ and the AutoCAD .NET API (Part 5)

ModelSpace | PaperSpace | CurrentSpace

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

Introduction

In today's post we want to have a look at some further objects that easily can be made queryable. The model space is probably the first of such objects that comes to mind. So what's the standard way to access the model space? Something like this:


[CommandMethod("ProcessModelSpaceEntities")]
public void ProcessModelSpaceEntities()
{
  var db = HostApplicationServices.WorkingDatabase;

  using (var tr = db.TransactionManager.StartTransaction())
  {
    var blockTable = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
    var modelSpace = (BlockTableRecord)tr.GetObject(blockTable[BlockTableRecord.ModelSpace], OpenMode.ForRead);

    foreach (var entityID in modelSpace)
    {
      var entity = (Entity)tr.GetObject(entityID, OpenMode.ForRead);
      // Do something with entity
    }
  }
}

Listing 1: ProcessModelSpaceEntities

ModelSpace to the front row

The model space is a frequently used object, so it is quite unintuitive that is burried as a special BlockTableRecord in the BlockTable. And again, we have to write a lot of boilerplate code to get to the actual model space entities. For our GeneralHelper it would be nice to have the model space in the front row, making querying for entities easy and the code to do that short. Something like this would be nice:


[CommandMethod("PrintLineCount")]
public static void PrintLineCount()
{
  using (var helper = new GeneralHelper())
  {
    var numberOfLines = helper.ModelSpace
                              .OfType<Line>()
                              .Count();
    Application.ShowAlertDialog("There are " + numberOfLines + " lines in the model space");
  }
}

Listing 2: PrintLineCount

Once we have an instance of the GeneralHelper, we only need to write a single line of code to get the number of lines in the model space.

So how do we implement this functionality? Well, the code needed is actually already in listing 1, so for starters we take that code and put it into a property called ModelSpace:


public IEnumerable<Entity> ModelSpace
{
  get
  {
    var blockTable = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
    var modelSpace = (BlockTableRecord)tr.GetObject(blockTable[BlockTableRecord.ModelSpace], OpenMode.ForRead);
  
    foreach (var entityID in modelSpace)
    {
      yield return (Entity)tr.GetObject(entityID, OpenMode.ForRead);
    }
  }
}

Listing 3: ModelSpace

And that's it. The type of the property is IEnumerable<Entity> and we use yield return to return the single entities. So when we add this property to the GeneralHelper, we're able to run the line count example in listing 2.

Paper space and current space

And what about the paper space? Layouts have a geometric representation as well, how can we access it? Similar to the ModelSpace property we used in line 6 of the code above, the BlockTableRecord class has another static property named PaperSpace, which points to the last active paper space layout. Furthermore, there's another interesting property for us: the database object has a property named CurrentSpaceId, which represents the ID of the currently selected layout (so this is either the model space or one of the paper space layouts).

Having that information, we can now refactor the ModelSpace property from listing 3: let's make the code more general and put it into a method, so that we can get the entities of the block by providing the name or the ObjectId of the according BlockTableRecord:


private IEnumerable<Entity> GetEntities(Func<BlockTable, ObjectId> getBlockID)
{
  var blockTable = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
  var block = (BlockTableRecord)tr.GetObject(getBlockID(blockTable), OpenMode.ForRead);

  foreach (var entityID in block)
  {
    yield return (Entity)tr.GetObject(entityID, OpenMode.ForRead);
  }
}

Listing 4: GetEntities

The GetEntities method takes a function as a parameter, which takes the BlockTable as an argument and returns the ObjectId of the BlockTableRecord we want to query. Using the function as an argument we can return the BlockTableRecord from either the name or the ObjectId.

Now we have a modified ModelSpace property and two new properties, one pointing to the last active paper space layout, and the other one to the currently active layout:


public IEnumerable<Entity> ModelSpace
{
  get { return GetEntities(table => table[BlockTableRecord.ModelSpace]); }
}

public IEnumerable<Entity> PaperSpace
{
  get { return GetEntities(table => table[BlockTableRecord.PaperSpace]); }
}

public IEnumerable<Entity> CurrentSpace
{
  get { return GetEntities(table => table[db.CurrentSpaceId]); }
}

Listing 5: ModelSpace, PaperSpace and CurrentSpace

Looks good! But, as always, there's room for improvement: There is always exactly one model space layout in a drawing, but we can have several paper space layouts. With the current implementation, we can access only the last active paper space layout. Since paper space layouts come with a name and have a certain position in the tab control at the bottom of the viewport, it would be nice to get a paper space layout by providing either the name or the tab index.

To implement that, our GetEntities method is sufficient and we just have to use it correctly. To get a layout by tab index we can do the following:


public IEnumerable<Entity> PaperSpace(int index)
{
  return GetEntities(t =>
                     {
                       var layout = Layouts.FirstOrDefault(l => l.TabOrder == index);
                       
                       if (layout == null)
                       {
                         throw new ArgumentException("No layout with tab index " + index + " found");
                       }
                       
                       return layout.BlockTableRecordId;
                     });
}

Listing 6: PaperSpace method by tab index

First we have to consider that an element of type Layout (accessible in the GeneralHelper via the Layouts property in line 5) stores characteristics of a paper space layout, like the tab index we are interested in. Each Layout object has a corresponding object of type BlockTableRecord, that holds the geometry of that layout. In other words, the BlockTableRecord is the object we get our entities from. Now the trick in the above implementation is, that we first look for the Layout that has the desired tab index, and use the Layout's BlockTableRecordId property to get get to the BlockTableRecord.

Getting a paper space layout by name is not different from getting the model space layout, so it's actually the same code as in listing 5. But it's better to add an overload of GetEntities to check the layout name and throw an exception if it is unknown:


private IEnumerable<Entity> GetEntities(string name)
{
  return GetEntities(t =>
                     {
                       if (!t.Has(name))
                       {
                         throw new ArgumentException("Layout does not exist");
                       }
                       
                       return t[name];
                     });
}

Listing 7: GetEntities

Put together we now have the following new code for our GeneralHelper:


public IEnumerable<Entity> CurrentSpace
{
  get { return GetEntities(table => db.CurrentSpaceId); }
}

public IEnumerable<Entity> ModelSpace
{
  get { return GetEntities(BlockTableRecord.ModelSpace); }
}

public IEnumerable<Entity> PaperSpace()
{
  return GetEntities(BlockTableRecord.PaperSpace);
}

public IEnumerable<Entity> PaperSpace(string name)
{
  return GetEntities(name);
}

public IEnumerable<Entity> PaperSpace(int index)
{
  return GetEntities(t =>
                     {
                       var layout = Layouts.FirstOrDefault(l => l.TabOrder == index);
                       
                       if (layout == null)
                       {
                         throw new ArgumentException("No layout with tab index " + index + " found");
                       }
                       
                       return layout.BlockTableRecordId;
                     });
}

private IEnumerable<Entity> GetEntities(string name)
{
  return GetEntities(t =>
                     {
                       if (!t.Has(name))
                       {
                         throw new ArgumentException("Layout does not exist");
                       }
                       
                       return t[name];
                     });
}

private IEnumerable<Entity> GetEntities(Func<BlockTable, ObjectId> getBlockID)
{
  var blockTable = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
  var block = (BlockTableRecord)tr.GetObject(getBlockID(blockTable), OpenMode.ForRead);

  foreach (var entityID in block)
  {
    yield return (Entity)tr.GetObject(entityID, OpenMode.ForRead);
  }
}

Listing 8: GeneralHelper Methods

So the public interface of the GeneralHelper class has two new properties and three new methods:

  • A property to query the space that is currently active
  • A property to query the model space
  • Three methods to query the paper space layouts:
    • The last one that was active
    • One with a specific name
    • One with a specific tab index

What's next?

We now have a neat way to query most of the containers of the drawing database. But at the moment all objects are opened "for read". The next step is to incorporate write access, so we can make changes to the objects. We’ll look at that in the next post.