LINQ and the AutoCAD .NET API (Part 5)

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.

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:

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
  }
}

The model space is an object that frequently is used, so it is quite unintuitive that is burried as a special block in the block table. And again, we have to write a lot of boiler plate 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");
  }
}

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

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.

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

The GetEntities method takes a function as 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(talbe => table[BlockTableRecord.ModelSpace]); }
}

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

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

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(table =>
                    {
                      var layout = Layouts.FirstOrDefault(l => l.TabOrder == index);

                      if (layout == null)
                      {
                        throw new ArgumentException("No layout with tab index " + index + " found");
                      }

                      return layout.BlockTableRecordId;
                    });
}

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 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(table =>
                    {
                      if (!table.Has(name))
                      {
                        throw new ArgumentException("Layout does not exist");
                      }

                      return table[name];
                    });
}

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(table =>
                    {
                      if (!table.Has(name))
                      {
                        throw new ArgumentException("Layout does not exist");
                      }

                      return table[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);
  }
}

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.

Advertisements

3 thoughts on “LINQ and the AutoCAD .NET API (Part 5)

  1. Hello Mr Wolfgang

    There is a part I am not clear on:

    Listing 4, line 4 confuses me.We take the block table as an input (and the only input). Why not just hard code the value rather than using a func?

    rgds

    Ben

    • actually never mind I worked the above question out. What about line 12 on listing 6 – what is the difference between returning an ObjectID and BlockTableRecordID?

      forgive me, so many questions!

  2. Hi Ben,

    a good point. You’re right, it’s actually not necessary to pass a function. It would be enough to pass the ObjectId. I chose the function approach to be able to do some additional checks before returning the ObjectId (like cheking the name in listing 7). But the check could be done as well outside the of the function. So the listings could be rewritten like:

    Listing 4:

    private IEnumerable<Entity> GetEntities(ObjectId blockID)
    {
      var blockTable = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
      var block = (BlockTableRecord)tr.GetObject(blockID, OpenMode.ForRead);
     
      foreach (var entityID in block)
      {
        yield return (Entity)tr.GetObject(entityID, OpenMode.ForRead);
      }
    }
    

    Listing 5:

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

    Listing 6:

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

    Thanks for your comment!

    -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