Creating a selection filter that finds dynamic blocks in AutoCAD using .NET

An interesting question came in via email from Rob Outman. He's interested in applying a selection filter when the user selects dynamic blocks. This is straightforward for unmodified dynamic blocks – just as with standard blocks, you can filter on the block name very easily – but it works less well on dynamic blocks whose properties have been modified at an instance level.

Essentially what happens is this: if you select a block reference to a dynamic block in the AutoCAD editor and then use (for example) the Properties window to edit some of the custom properties associated with that block, the block definition gets duplicated as an anonymous block – with the modified properties, of course – and the reference gets updated to point to that. If you LIST the block reference, you can see that it still mentions the originating block by name, so it's clear some connection still exists between the two block definitions, at the very least.

Listing the properties of a modified dynamic block reference

This makes it a little difficult to use a SelectionFilter to look for these BlockReference objects, as their associated BlockTableRecord has a name such as "*U24" rather than the name being searched for.

The answer to this riddle was actually reasonably straightforward, in the end. Using the very handy ArxDbg sample, it was easy to find out that the modified block definition contains XData linking back to the original – under the AcDbBlockRepBTag app name there's an entry containing its handle – which we can use to compile a list of the (anonymous) names of modified blocks for which to look.

ArxDbg showing the XData linking modified dynamic blocks to their originals

We can then create a conditional SelectionFilter – with an "or" clause listing each of these names – and use that to find any block meeting the condition.

Here's the C# code that does all this:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using System.Collections.Generic;

 

namespace EntitySelection

{

  public class Commands

  {

    [CommandMethod("SDB")]

    static public void SelectDynamicBlocks()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      var ed = doc.Editor;

 

      var pso =

        new PromptStringOptions(

          "\nName of dynamic block to search for"

        );

      pso.AllowSpaces = true;

 

      var pr = ed.GetString(pso);

 

      if (pr.Status != PromptStatus.OK)

        return;

 

      string blkName = pr.StringResult;

 

      List<string> blkNames = new List<string>();

      blkNames.Add(blkName);

 

      var tr = doc.TransactionManager.StartTransaction();

      using (tr)

      {

        var bt =

          (BlockTable)tr.GetObject(

            doc.Database.BlockTableId,

            OpenMode.ForRead

          );

 

        // Start by getting the handle of our block, if it exists

 

        if (!bt.Has(blkName))

        {

          ed.WriteMessage(

            "\nCannot find block called \"{0}\".", blkName

          );

          return;

        }

 

        var btr =

          (BlockTableRecord)tr.GetObject(

            bt[blkName], OpenMode.ForRead

          );

 

        var blkHand = btr.Handle;

 

        foreach (var bid in bt)

        {

          // We'll check each block in turn, to see if it has

          // XData pointing to our original block definition

 

          var btr2 =

            (BlockTableRecord)tr.GetObject(bid, OpenMode.ForRead);

 

          // Only check blocks that don't share the name 🙂

 

          if (btr2.Name != blkName)

          {

            // And only check blocks with XData

 

            var xdata = btr2.XData;

            if (xdata != null)

            {

              // Get the XData as an array of TypeValues and loop

              // through it

 

              var tvs = xdata.AsArray();

              for (int i=0; i < tvs.Length; i++)

              {

                // The first value should be the RegAppName

 

                var tv = tvs[i];

                if (

                  tv.TypeCode == (int)DxfCode.ExtendedDataRegAppName

                )

                {

                  // If it's the one we care about...

 

                  if ((string)tv.Value == "AcDbBlockRepBTag")

                  {

                    // ... then loop through until we find a

                    // handle matching our blocks or otherwise

                    // another RegAppName

 

                    for (int j = i + 1; j < tvs.Length; j++)

                    {

                      tv = tvs[j];

                      if (

                        tv.TypeCode ==

                          (int)DxfCode.ExtendedDataRegAppName

                      )

                      {

                        // If we have another RegAppName, then

                        // we'll break out of this for loop and

                        // let the outer loop have a chance to

                        // process this section

 

                        i = j - 1;

                        break;

                      }

 

                      if (

                        tv.TypeCode ==

                          (int)DxfCode.ExtendedDataHandle

                      )

                      {

                        // If we have a matching handle...

 

                        if ((string)tv.Value == blkHand.ToString())

                        {

                          // ... then we can add the block's name

                          // to the list and break from both loops

                          // (which we do by setting the outer index

                          // to the end)

 

                          blkNames.Add(btr2.Name);

                          i = tvs.Length - 1;

                          break;

                        }

                      }

                    }

                  }

                }

              }

            }

          }

        }

 

        tr.Commit();

      }

 

      // Build a conditional filter list so that only

      // entities with the specified properties are

      // selected

 

      SelectionFilter sf =

        new SelectionFilter(CreateFilterListForBlocks(blkNames));

      < /span>PromptSelectionResult psr = ed.SelectAll(sf);

 

      ed.WriteMessage(

        "\nFound {0} entit{1}.",

        psr.Value.Count,

        (psr.Value.Count == 1 ? "y" : "ies")

      );

    }

 

    private static TypedValue[] CreateFilterListForBlocks(

      List<string> blkNames

    )

    {

      // If we don't have any block names, return null

 

      if (blkNames.Count == 0)

        return null;

 

      // If we only have one, return an array of a single value

 

      if (blkNames.Count == 1)

        return new TypedValue[] {

          new TypedValue(

            (int)DxfCode.BlockName,

           blkNames[0]

          )

        };

 

      // We have more than one block names to search for...

 

      // Create a list big enough for our block names plus

      // the containing "or" operators

 

      List<TypedValue> tvl =

        new List<TypedValue>(blkNames.Count + 2);

 

      // Add the initial operator

 

      tvl.Add(

        new TypedValue(

          (int)DxfCode.Operator,

          "<or"

        )

      );

 

      // Add an entry for each block name, prefixing the

      // an
onymous block names with a reverse apostrophe

 

      foreach (var blkName in blkNames)

      {

        tvl.Add(

          new TypedValue(

            (int)DxfCode.BlockName,

            (blkName.StartsWith("*") ? "`" + blkName : blkName)

          )

        );

      }

 

      // Add the final operator

 

      tvl.Add(

        new TypedValue(

          (int)DxfCode.Operator,

          "or>"

        )

      );

 

      // Return an array from the list

 

      return tvl.ToArray();

    }

  }

}

An interesting point to note: we need to prefix any anonymous block name with an inverted apostrophe ("`") character, as that seems to be the way to "escape" asterisks in this situation.

Let's see this in action. We'll start by loading the standard "Dynamic Blocks/Annotation – Metric.dwg" sample into AutoCAD, and inserting a number of "Elevation – Metric" dynamic blocks into the drawing:

Five identical block references - with the same definition

We can then use the Properties window to edit the Custom properties on each:

Modifying the symbol rotation of one of the blocks

Which leads to branched block definitions:

Our modified dynamic blocks

We can then use the SDB command to select our the block references for both the original dynamic block definition and its modified, branched copies.

Command: SDB

Name of dynamic block to search for: Elevation - Metric

Found 5 entities.

Update:

Thanks to Roland Feletic for pointing me to the GetAnonymousBlockIds() function that I'd manage to overlook or forget about. Roland passed on some code he'd received from Tony Tanzillo, so thanks to Tony, too (although all I took was the method name :-).

The use of this function does simplify the code nicely, as well as reducing the dependence on the XData link (which is ultimately an implementation detail).

Here's the updated C# code, which works in the same way as the prior version:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.Runtime;

using Autode
sk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using System.Collections.Generic;

 

namespace EntitySelection

{

  public class Commands

  {

    [CommandMethod("SDB")]

    static public void SelectDynamicBlocks()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      var ed = doc.Editor;

 

      var pso =

        new PromptStringOptions(

          "\nName of dynamic block to search for"

        );

      pso.AllowSpaces = true;

 

      var pr = ed.GetString(pso);

 

      if (pr.Status != PromptStatus.OK)

        return;

 

      string blkName = pr.StringResult;

 

      List<string> blkNames = new List<string>();

      blkNames.Add(blkName);

 

      var tr = doc.TransactionManager.StartTransaction();

      using (tr)

      {

        var bt =

          (BlockTable)tr.GetObject(

            doc.Database.BlockTableId,

            OpenMode.ForRead

          );

 

        // Start by getting access to our block, if it exists

 

        if (!bt.Has(blkName))

        {

          ed.WriteMessage(

            "\nCannot find block called \"{0}\".", blkName

          );


60;        
return;

        }

 

        // Get the anonymous block names

        var btr =

          (BlockTableRecord)tr.GetObject(

            bt[blkName], OpenMode.ForRead

          );

 

        if (!btr.IsDynamicBlock)

        {

          ed.WriteMessage(

            "\nCannot find a dynamic block called \"{0}\".", blkName

          );

          return;

        }

 

        // Get the anonymous blocks and add them to our list

 

        var anonBlks = btr.GetAnonymousBlockIds();

        foreach (ObjectId bid in anonBlks)

        {

          var btr2 =

            (BlockTableRecord)tr.GetObject(bid, OpenMode.ForRead);

 

          blkNames.Add(btr2.Name);

        }

 

        tr.Commit();

      }

 

      // Build a conditional filter list so that only

      // entities with the specified properties are

      // selected

 

      SelectionFilter sf =

        new SelectionFilter(CreateFilterListForBlocks(blkNames));

      PromptSelectionResult psr = ed.SelectAll(sf);

 

      ed.WriteMessage(

        "\nFound {0} entit{1}.",

        psr.Value.Count,

        (psr.Value.Count == 1 ? "y" : "ies")

      );

    }

 

    private static TypedValue[] CreateFilterListForBlocks(

      List<string> blkNames

    )

    {

      // If we don't have any block names, return null

 

      if (blkNames.Count == 0)

        return null;

 

      // If we only have one, return an array of a single value

 

      if (blkNames.Count == 1)

        return new TypedValue[] {

          new TypedValue(

            (int)DxfCode.BlockName,

           blkNames[0]

          )

        };

 

      // We have more than one block names to search for...

 

      // Create a list big enough for our block names plus

      // the containing "or" operators

 

      List<TypedValue> tvl =

        new List<TypedValue>(blkNames.Count + 2);

 

      // Add the initial operator

 

      tvl.Add(

        new TypedValue(

          (int)DxfCode.Operator,

          "<or"

        )

      );

 

      // Add an entry for each block name, prefixing the

      // anonymous block names with a reverse apostrophe

 

      foreach (var blkName in blkNames)

      {

        tvl.Add(

          new TypedValue(

            (int)DxfCode.BlockName,

            (blkName.StartsWith("*") ? "`" + blkName : blkName)

          )

   
0;    );

      }

 

      // Add the final operator

 

      tvl.Add(

        new TypedValue(

          (int)DxfCode.Operator,

          "or>"

        )

      );

 

      // Return an array from the list

 

      return tvl.ToArray();

    }

  }

}

15 responses to “Creating a selection filter that finds dynamic blocks in AutoCAD using .NET”

  1. This post is timely, I just need this kind of approach for a new application. I had not thought about the extended data. Thank you.

  2. Hi Kean,

    What is the difference between what you have, and using btr.GetAnonymousBlockIds() or btr.GetAnonymousBlockIds(bool, bool)?

    Thanks,

    Brent

  3. That should have read btr.GetBlockReferenceIds(bool, bool)

  4. Hi Brent,

    GetBlockReferenceIds() will give you back all the block references (whether direct or nested, depending on the arguments) for a particular block definition.

    The requirement for this post was to provide a selection filter - which can be used interactively, even if in this post it's shown using Editor.SelectAll() - to deal with dynamic blocks whose name may have changed due to editing.

    So the code in this post meets a very different need.

    Regards,

    Kean

  5. Thanks for that Kean!!

  6. Please, how to insert this code into AutoCAD (step-by-step)?

  7. This post should help.

    Kean

  8. Hi Kean...

    I have a question...

    I have a block and I want find entities like shape the block...

    The entities are not block...

    How to find the entities???

    Can you answer advice to me ???

  9. Hi Gimin,

    As mentioned via Twitter, I generally answer questions that relate specifically to blog posts (which this one does not).

    You might want to check the geometry of the block you're looking for (e.g. the extents and lengths of curves) and then check that against similarly configured geometry in the drawing. There would be quite a lot to take into account of not just looking for simple "explodes", though.

    Please ask future questions via the online discussion forums.

    Regards,

    Kean

  10. Hi Kean,

    I have a question- I have to convert the below LISP code in .NET code but I don't find any similar method to achieve the same.
    ---(if (=
    (cdr (assoc 0 (entget (cdr (assoc -2 BLOCKLIST)))))
    "ENDBLK" )
    (progn
    (if (setq NB (ssget "x" (list (assoc 2 BLOCKLIST))))
    (command "erase" NB "" )
    )
    )

    I am stuck with "ENDBLK"
    How to filter the blocks with "ENDBLK" from the Block LIST?

    I have tried a lot to find but I could not get any relevant information.

    Please help me out on this.
    Thanks.

  11. Dagmar de Jonge Avatar

    Chr(34) used to use the * sign?

    1. Sorry, Dagmar - could you restate your question? If it isn't directly relevant to this post, please submit it to the relevant online discussion forum.

      Thanks,

      Kean

  12. I don't think it's necessary to use the 'or' clause. You should be able to just list the block names separated by commas in a single typed value entry (and maybe one more to add the object type of block reference). I haven't tried that in .NET API, but it works with anonymous block names in the FILTER command I have tried this trick with layer names in the COM API. Thanks for the tip on escaping the asterisk with the grave accent - if I knew that, I had forgotten.

  13. Лена Пач Avatar
    Лена Пач

    Hello. Your example is very useful. However, I still did not understand why you are using inverted apostrophe ("` ") character:
    (blkName.StartsWith ("*")? "` "+ blkName: blkName):
    Thank you.

    1. Kean Walmsley Avatar

      It's escaping the asterisk at the beginning of the block name.

      Kean

Leave a Reply to Gimin Park Cancel reply

Your email address will not be published. Required fields are marked *