Thursday, April 7, 2011

Tips of Using LINQ in AutoCAD .NET programming

If you target your AutoCAD .NET API programming at .NET3.5 or 4.0, using LINQ would sometimes make your coding a lot easier. Typically, writing code to loop through various collection objects in drawing database is one of mostly encountered tasks of AutoCAD programmers.

I demonstrate here some sample code that uses LINQ (to object) to make the code simpler and easier.

BlockTableRecord class is one the mostly used class an AutoCAD programmer may have to deal with. It implements IEnumerale interface. So, we have seen a lots of "foreach(...)" loop used against it in AutoCAD .NET code, especially when ModelSpace BlockTableRecord is opened for searching certain type of entities contained in ModelSpace. Here are some cases.

Case 1:  searching certain types of entities in BlockTableRecord (ModelSpace, PaperSpace, or a block definition).

private List<ObjectId> GetCertainEntityIDs(Database db)
{
    List<ObjectId> ids = null;
 
    using (Transaction tran = db.TransactionManager.StartTransaction())
    {
        BlockTable tbl = 
            (BlockTable)tran.GetObject(db.BlockTableId, OpenMode.ForRead);
 
        //Get modelspace BloclTableRecord
        BlockTableRecord br = 
            (BlockTableRecord)tran.GetObject(tbl[BlockTableRecord.ModelSpace], OpenMode.ForRead);
 
        //Cast the BlockTableRecord into IEnumeralbe<T> collection 
        IEnumerable<ObjectId> b = br.Cast<ObjectId>();
 
        //==============search certain entity========================//
        //"LINE" for line
        //"LWPOLYLINE" for polyline
        //"CIRCLE" for circle
        //"INSERT" for block reference
        //...
        //We can use "||" (or) to search for more then one entity types
        //============================================================//
 
        //Use lambda extension method
        ids = b.Where(id => id.ObjectClass.DxfName.ToUpper() == "LINE" || 
            id.ObjectClass.DxfName.ToUpper() == "LWPOLYLINE").ToList<ObjectId>();
 
        //Use LINQ statement. This is more readable
        ids = (from id in b
                where id.ObjectClass.DxfName.ToUpper()=="LINE" ||
                        id.ObjectClass.DxfName.ToUpper() == "LWPOLYLINE"
                select id).ToList<ObjectId>();
 
        tran.Commit();
    }
 
    return ids;
}

Case 2: often, we need to get a distinctive entity type list from a drawing (e.g. we need to know what types of entities the drawing's model space contains). Following code get a string array of entity types from model space.

private static string[] GetEntityType(Document dwg)
{
    string[] types = null;
 
    Database db = dwg.Database;
    using (Transaction tran = db.TransactionManager.StartTransaction())
    {
        BlockTable tbl = 
            (BlockTable)tran.GetObject(db.BlockTableId, OpenMode.ForRead);
 
        //Get modelspace BloclTableRecord
        BlockTableRecord br = 
            (BlockTableRecord)tran.GetObject(tbl[BlockTableRecord.ModelSpace], OpenMode.ForRead);
 
        //Cast the BlockTableRecord into IEnumeralbe<T> collection
        IEnumerable<ObjectId> b = br.Cast<ObjectId>();
 
        //Use lambda extension method
        types = b.Select(id => id.ObjectClass.DxfName).Distinct().ToArray();
 
        //Use LINQ statement. This is more readable
        types = (from id in b select id.ObjectClass.DxfName).Distinct().ToArray();
 
        tran.Commit();
    }
 
    return types;
}

Case 3: getting a name list of blocks that have been inserted into model space, or get all block references or block references with given names in model space.

From the code show above, we can see ObjectClass property of ObjectId struct, which was only made available since AutoCAD 2009, helps a lot in defining searching condition (in Where() or Distinct() extension methods). In this scenario, we need a extra method to get to entity itself. Here is it:

private static BlockReference GetBlockRef(ObjectId id, Transaction tran)
{
    return (tran.GetObject(id, OpenMode.ForRead) as BlockReference);
}

Now we can get a name list of all inserted blocks:

private static string[] GetBlockNameList(Document dwg)
{
    string[] names = null;
 
    Database db = dwg.Database;
    using (Transaction tran = db.TransactionManager.StartTransaction())
    {
        BlockTable tbl = 
            (BlockTable)tran.GetObject(db.BlockTableId, OpenMode.ForRead);
 
        //Get modelspace BloclTableRecord
        BlockTableRecord br = 
            (BlockTableRecord)tran.GetObject(tbl[BlockTableRecord.ModelSpace], OpenMode.ForRead);
 
        //Cast the BlockTableRecord into IEnumeralbe<T> collection
        IEnumerable<ObjectId> b = br.Cast<ObjectId>();
 
        //Use lambda extension method
        names=b.Where(id=>id.ObjectClass.DxfName.ToUpper()=="INSERT")
                .Select(id=>GetBlockRef(id,tran).Name).Distinct().ToArray();
 
        //Use LINQ statement. This is more readable
        names = (from id in b
                where id.ObjectClass.DxfName.ToUpper().Contains("INSERT")
                select GetBlockRef(id, tran).Name).Distinct().ToArray();
 
        tran.Commit();
    }
 
    return names;
}

To search blocks with given name, we do this:

private static List<BlockReference> GetBlockRefs(string blkName, Document dwg)
{
    List<BlockReference> blks = null;
 
    Database db = dwg.Database;
    using (Transaction tran = db.TransactionManager.StartTransaction())
    {
        BlockTable tbl = 
            (BlockTable)tran.GetObject(db.BlockTableId, OpenMode.ForRead);
 
        //Get modelspace BloclTableRecord
        BlockTableRecord br = 
            (BlockTableRecord)tran.GetObject(tbl[BlockTableRecord.ModelSpace], OpenMode.ForRead);
 
        //Cast the BlockTableRecord into IEnumeralbe<T> collection
        IEnumerable<ObjectId> b = br.Cast<ObjectId>();
 
        //Use lambda extension method
        blks = b.Where(id => id.ObjectClass.DxfName.ToUpper()=="INSERT")
            .Select(id => GetBlockRef(id, tran)).ToList<BlockReference>();
 
        //Use LINQ statement. This is more readable
        blks = (from id in b
                where id.ObjectClass.DxfName.ToUpper()=="INSERT"
                select GetBlockRef(id, tran)).ToList<BlockReference>();
 
        tran.Commit();
    }
 
    //return only blockreferences with name starting with blkName with lambda extension method
    return blks.Where(b=>b.Name.ToUpper()==blkName.ToUpper()).ToList();
 
    //Or return with LINQ statement
    return (from b in blks where b.Name.ToUpper()==blkName.ToUpper() select b).ToList();
}

Note, the last code sample "GetBlockRefs()" is only for demonstrating the use of LINQ. It may not be a good idea in certain situation to return entity instead of ObjectId outside a transaction.

As you can see, with the help of LINQ, we can write more readable code easily to search drawing database. However, what is the difference in term of code execution speed between simply looping through the model space with foreach (...) and using LINQ as I showed here? I did not look into this, honestly.I did run the code with drawing with a few thousands entities in model space and did not feel speed difference. But I assume there is difference with LINQ code might result in slightly slower speed: when casting BlockTableRecord into IEnumerable, a loop must have been done behind the scene. If you have very big model with hundreds of thousands entities, you want to test if the LINQ code actually slows down the execution noticeably.

Followers

About Me

My photo
After graduating from university, I worked as civil engineer for more than 10 years. It was AutoCAD use that led me to the path of computer programming. Although I now do more generic business software development, such as enterprise system, timesheet, billing, web services..., AutoCAD related programming is always interesting me and I still get AutoCAD programming tasks assigned to me from time to time. So, AutoCAD goes, I go.