Author Topic: Iterating through block definitions by name  (Read 16020 times)

0 Members and 1 Guest are viewing this topic.

cannorth

  • Guest
Iterating through block definitions by name
« on: March 23, 2012, 01:02:42 PM »
Hello,

  Can anyone send me code about iterating through block definitions by name using VB.Net ObjectARX?

Thanks,

cannorth

n.yuan

  • Bull Frog
  • Posts: 348
Re: Iterating through block definitions by name
« Reply #1 on: March 23, 2012, 03:47:37 PM »
The fact is, given a drawing, you need to open each BlockTablerecord to get block name. That is, by the time you have a list of block names, the BlockTable has already been iterated at least once.
 
So I thought you may want to look into how to get a block name list first. Here is some code doing that:

Code: [Select]
public static string[] GetBlockNamesInDrawing(Document dwg)
{
    List<string> blkNames = null;
    using (Transaction tran = dwg.Database.TransactionManager.StartTransaction())
    {
        BlockTable tbl = (BlockTable)tran.GetObject(dwg.Database.BlockTableId, OpenMode.ForRead);

        IEnumerable<ObjectId> blkIDs = tbl.Cast<ObjectId>();
        var blks = from id in blkIDs
                   where !id.IsEffectivelyErased && !id.IsNull && !id.IsErased
                   select GetBlockTableRecordFromId(id, tran);

        blkNames = (from b in blks
                    where !b.IsAnonymous &&
                    !b.IsAProxy &&
                    !b.IsFromExternalReference &&
                    !b.IsFromOverlayReference &&
                    !b.IsLayout
                    orderby b.Name ascending
                    select b.Name).ToList();
       
        tran.Commit();
    }

    return blkNames == null ? null : blkNames.ToArray();
}

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

« Last Edit: March 23, 2012, 03:54:59 PM by n.yuan »

TheMaster

  • Guest
Re: Iterating through block definitions by name
« Reply #2 on: March 24, 2012, 04:25:31 AM »
Code: [Select]

....
        IEnumerable<ObjectId> blkIDs = tbl.Cast<ObjectId>();
        var blks = from id in blkIDs
                   where !id.IsEffectivelyErased && !id.IsNull && !id.IsErased
                   select GetBlockTableRecordFromId(id, tran);


A couple of points, if you don't mind:

You don't need any of those tests (IsEffectivelyErased, IsErased, or IsNull)
because none of them can be true. If you iterate over the IncludingErased
property of the BlockTable, only then are erased ObjectIds included in the
enumeration. Otherwise, you will not encounter erased ObjectIds

IsEffectivelyErased means that one or more of the object's owners is
erased, which could not be possible in this case, because the BlockTable
(which is the owner of all BlockTableRecords) isn't erased.

IsNull can't be true if the object is database-resident.

Another tip:

You could replace GetBlockTableRecordFromId() with
a generic extension method like:

Code - C#: [Select]
  1.  
  2.    public static T GetObject<T>( this ObjectId id )
  3.      where T: DBObject
  4.    {
  5.         return (T) id.GetObject( OpenMode.ForRead, false, false );
  6.    }
  7.  
  8.    // usage:
  9.  
  10.    ObjectId id = // assign to ID of a blockTableRecord
  11.  
  12.    BlockTableRecord btr = id.GetObject<BlockTableRecord>();
  13.  
  14.    // You can use the same extension method to open any
  15.    // type of entity. For example, this opens the first entity
  16.    // in the block:
  17.  
  18.    Entity entity = btr.Cast<ObjectId>().First().GetObject<Entity>();
  19.  
  20.  

And one last tip:

Notice that the generic extension method above calls the overload of
ObjectId.GetObject() that takes all arguments. If you look at the
source for ObjectId.GetObject() in Reflector or ILSpy, you'll notice
that all of the overloaded versions delegate to that same overload (the
one that takes all arguments).

So, by calling that same overload directly, we can avoid a needless
level of indirection.



//
edit:kdub - formatting code = csharp
« Last Edit: March 26, 2012, 03:40:49 PM by Kerry »

gile

  • Gator
  • Posts: 2507
  • Marseille, France
Re: Iterating through block definitions by name
« Reply #3 on: March 24, 2012, 10:06:54 AM »
I really like these generic extension methods.

Here's an example (thanks again to kaefer)

Code: [Select]
    public static class Extensions
    {
        public static T GetObject<T>(this ObjectId id) where T : DBObject
        {
            return id.GetObject(OpenMode.ForRead, false, false) as T;
        }

        public static IEnumerable<T> GetObjects<T>(this IEnumerable ids) where T : DBObject
        {
            return ids
                .Cast<ObjectId>()
                .Select(id => id.GetObject(OpenMode.ForRead, false, false))
                .OfType<T>();
        }
    }

An the GetBlockNamesInDrawing() method should be this:

Code: [Select]
        public static string[] GetBlockNamesInDrawing(Database db)
        {
            using (Transaction tr = db.TransactionManager.StartTransaction())
            {
                return db.BlockTableId
                    .GetObject<BlockTable>()
                    .GetObjects<BlockTableRecord>()
                    .Where(btr =>
                        !btr.IsAnonymous &&
                        !btr.IsFromExternalReference &&
                        !btr.IsFromOverlayReference &&
                        !btr.IsLayout &&
                        !btr.IsAProxy)
                    .Select(btr => btr.Name)
                    .ToArray();
            }
        }
Speaking English as a French Frog

TheMaster

  • Guest
Re: Iterating through block definitions by name
« Reply #4 on: March 26, 2012, 01:49:23 AM »
I really like these generic extension methods.

Here's an example (thanks again to kaefer)

Code - C#: [Select]
  1.    
  2.     public static class Extensions
  3.     {
  4.         public static T GetObject<T>(this ObjectId id) where T : DBObject
  5.         {
  6.             return id.GetObject(OpenMode.ForRead, false, false) as T;
  7.         }
  8.  
  9.         public static IEnumerable<T> GetObjects<T>(this IEnumerable ids) where T : DBObject
  10.         {
  11.             return ids
  12.                 .Cast<ObjectId>()
  13.                 .Select(id => id.GetObject(OpenMode.ForRead, false, false))
  14.                 .OfType<T>();
  15.         }
  16.     }
  17.  
  18.  

Since I wrote the first versions of these functions many years ago, I've since made some improvements in the interest of performance, and in dealing with other issues like locked layers.

First, I don't agree with the idea that a GetObject<T>() method should return null if the object is not of the type of the generic argument. But that idea is predicated on testing the ObjectId's ObjectClass property before even trying to open the object, because to put it quite simply, it is faster.

So, you might note that my GetObject<T>() is assertive, meaning that it throws an exception if the opened object is not of the desired type.  That's because I only use it in cases where I expect the object to be of the type of the generic argument, and if it is not, that is a hard error that should result in an exception.

Coincidentally, I do have a version of the GetObject<T>() method you show above, except that mine is called TryGetObject<T>().  :wink:

And on the subject of opening many objects via GetObjects<T>(), here's mine (actually, a watered-down version of it with dependencies on other code I have here ripped out, and wasn't tested after editing it to remove the dependencies). This GetObjects<T>() is faster than any implementation that opens every object in the source and uses the 'as' operator to test if it is of the desired type, but feel free to scrutinize and/or test if you wish.

Code - C#: [Select]
  1.  
  2.  
  3. public static class AcDbExtensions
  4. {
  5.   // These overloads of GetObjects<T>() target BlockTableRecord
  6.   // specifically, because:
  7.   //
  8.   //   1. It is incorrect to target *any* IEnumerable, which
  9.   //      includes things like System.String. You shouldn't
  10.   //      allow an extension method to be invoked on a target
  11.   //      that is not applicable or elegible.
  12.   //
  13.   //   2. BlockTableRecord specifically contains only entities,
  14.   //      and supports opening erased entities, which requires
  15.   //      specialization that would unduly burden a more generic
  16.   //      solution targeting any IEnumerable that enumerates a
  17.   //      sequence of ObjectIds (e.g., ObjectIdCollection,
  18.   //      SymbolTableRecord, ObjectId[], etc). There are other
  19.   //      overloads of GetObjects<T>() which target those same
  20.   //      types and specialize them, without also carrying the
  21.   //      overhead of a single, 'all-purpose' solution ( those
  22.   //      latter ones are not included here ).
  23.  
  24.   public static IEnumerable<T> GetObjects<T>( this BlockTableRecord btr )
  25.     where T: Entity
  26.   {
  27.     return GetObjects<T>( btr, OpenMode.ForRead, false, false );
  28.   }
  29.  
  30.   public static IEnumerable<T> GetObjects<T>( this BlockTableRecord btr, OpenMode mode )
  31.     where T: Entity
  32.   {
  33.     return GetObjects<T>( btr, mode, false, false );
  34.   }
  35.  
  36.   public static IEnumerable<T> GetObjects<T>( this BlockTableRecord btr, bool openErased )
  37.     where T: Entity
  38.   {
  39.     return GetObjects<T>( btr, OpenMode.ForRead, openErased, false );
  40.   }
  41.  
  42.   public static IEnumerable<T> GetObjects<T>(
  43.     this BlockTableRecord btr,
  44.     OpenMode mode,
  45.     bool openErased,
  46.     bool openObjectsOnLockedLayers )  where T: Entity
  47.   {
  48.     // if caller wants erased objects, use the IncludingErase property:
  49.     BlockTableRecord source = openErased ? btr.IncludingErased : btr;
  50.  
  51.     // if the generic argument is <Entity>, there is no filtering
  52.     // needed because this method targets BlockTableRecord,
  53.     // whose elements are all entities:
  54.     bool unfiltered = typeof( T ) == typeof( Entity );
  55.    
  56.     bool openLocked = mode == OpenMode.ForWrite && openObjectsOnLockedLayers;
  57.  
  58.     // The runtime class of the generic argument managed wrapper type:
  59.     RXClass rxclass = RXClass.GetClass( typeof( T ) );
  60.  
  61.     // if no filtering is needed, just open and return every object
  62.     // cast to the generic argument type:
  63.  
  64.     if( unfiltered )
  65.     {
  66.       foreach( ObjectId id in source )
  67.       {
  68.         yield return (T) id.GetObject( mode, openErased, openLocked );
  69.       }
  70.     }
  71.     else     // caller wants a subset, so filter by runtime class:
  72.     {
  73.       foreach( ObjectId id in source )
  74.       {
  75.         if( id.ObjectClass.IsSubclassOf( rxclass ) )
  76.         {
  77.           yield return (T) id.GetObject( mode, openErased, openLocked );
  78.         }
  79.       }
  80.     }
  81.   }
  82.  
  83.  
  84.   //////////////////////////////////////////////////////////////////////////////
  85.   // Extension method:
  86.   //
  87.   //  IEnumerable<T>.UpgradeOpen<T>() where T: DBObject
  88.   //
  89.   // Upgrade each object to OpenMode.ForWrite as it is pulled
  90.   // from the sequence, which is preferable to opening everything
  91.   // for write, when we may not modify all opened objects.
  92.   //
  93.   // This method is useful when you want to upgrade the open mode
  94.   // of many objects, but do not want to modify objects that are on
  95.   // locked layers (that also presumes you are operating directly
  96.   // on a BlockTableRecord, possibly in a RealDwg application or a
  97.   // database that's not open in the drawing editor, and the source
  98.   // may include objects on locked layers).
  99.   //
  100.   // Objects on locked layers are skipped and omitted from the
  101.   // resulting sequence.
  102.  
  103.   public static IEnumerable<T> UpgradeOpen<T>( this IEnumerable<T> source )
  104.     where T: DBObject
  105.   {
  106.     foreach( T item in source )
  107.     {
  108.       try
  109.       {
  110.         if( ! item.IsWriteEnabled )    
  111.           item.UpgradeOpen();
  112.       }
  113.       catch( Autodesk.AutoCAD.Runtime.Exception ex )
  114.       {
  115.         if( ex.ErrorStatus != ErrorStatus.OnLockedLayer )
  116.           throw;
  117.         continue;
  118.       }
  119.       yield return item;
  120.     }
  121.   }
  122.  
  123.   // Helper extension method:  
  124.   // Open and return the model space BlockTableRecord for the
  125.   // given database:
  126.  
  127.   public static BlockTableRecord ModelSpace( this Database db )
  128.   {
  129.     return ModelSpace( db, OpenMode.ForRead );
  130.   }
  131.  
  132.   public static BlockTableRecord ModelSpace( this Database db, OpenMode mode )
  133.   {
  134.     return (BlockTableRecord) SymbolUtilityServices.GetBlockModelSpaceId( db )
  135.        .GetObject( mode, false, false );
  136.   }
  137.  
  138.   // Open and return the BlockTableRecord for the current space
  139.   // in the given database:
  140.  
  141.   public static BlockTableRecord CurrentSpace( this Database db )
  142.   {
  143.     return (BlockTableRecord) db.CurrentSpaceId.GetObject( OpenMode.ForRead, false, false );
  144.   }
  145.  
  146.   public static BlockTableRecord CurrentSpace( this Database db, OpenMode mode )
  147.   {
  148.     return (BlockTableRecord) db.CurrentSpaceId.GetObject( mode, false, false );
  149.   }
  150.  
  151.   // Extension Method: bool RXClass.IsSubclassOf( RXClass other )
  152.   //
  153.   // This helper avoids a managed-to-native transition in the
  154.   // case where the two runtime classes are the same class, by
  155.   // doing a comparison in managed code first, before calling
  156.   // IsDerivedFrom() (which is a native P/Invoke).
  157.   //
  158.   // When a significant precentage of the objects being filtered
  159.   // are of the same runtime class as the target, this will run
  160.   // faster than direct calls to IsDerivedFrom().
  161.   //
  162.   // In the latest version of the framework, P/Invoke is much
  163.   // faster and for that reason this helper may not be needed
  164.   // or will not offer any advantage over a direct call to the
  165.   // IsDerivedFrom() method.
  166.  
  167.   public static bool IsSubclassOf( this RXClass thisClass, RXClass otherClass )
  168.   {
  169.     return thisClass == otherClass || thisClass.IsDerivedFrom( otherClass );
  170.   }
  171.  
  172.  
  173. }
  174.  
  175. public static class Example
  176. {
  177.   // Erase all lines in model space that are < 0.1 units
  178.   // long. This example shows a more 'proper' way to use
  179.   // LINQ to write simple commands that modify entities.
  180.   // Specifically, it only opens objects for write if they are
  181.   // going to be modified, and gracefully deals with objects
  182.   // on locked layers. It should go without saying that we
  183.   // can do this same operation using a filtered selection
  184.   // as well, but only for the model or paper space blocks
  185.   // of drawings that are open in the drawing editor. For
  186.   // other blocks, or for operating on databases that aren't
  187.   // open in the drawing editor, an operation like this one
  188.   // must be handled using a pattern like that used here.
  189.   //
  190.   // Points of interest:
  191.   //
  192.   // The ModelSpace() extension method is called to
  193.   // open and get the model space BlockTableRecord.
  194.   //
  195.   // The GetObjects<Line>() extension method is called
  196.   // to get all Line entities in model space, open for read.
  197.   //
  198.   // The Where( line => line.Length < 0.1 ) call reduces
  199.   // the sequence of lines returned by GetObjects<Line>(),
  200.   // to only those lines whose length is < 0.1 units.
  201.   //
  202.   // The call to the UpgradeOpen() method receives the
  203.   // subset of entities returned by the call to Where(),
  204.   // and upgrades the open mode of those entities, which
  205.   // is the set of entities that will actually be erased.
  206.   // If any of those entities reside on locked layers, they
  207.   // will not be included in the sequence returned by the
  208.   // call to the UpgradeOpen() extension method.
  209.   //
  210.   // Because UpgradeOpen() uses deferred execution, the
  211.   // actual upgrading of each object's open mode does not
  212.   // happen until just before the object is assigned to the  
  213.   // 'line' variable within the foreach() loop.
  214.   //
  215.   // The ModelSpace() method is a handy shortcut for what
  216.   // is otherwise overly-verbose code that would only be
  217.   // correct in coding examples that many noobs mistakenly
  218.   // take too literaly, as representative examples of how
  219.   // real production code should be written.
  220.  
  221.  
  222.   [CommandMethod( "LINQ_WEEDLINES" )]
  223.   public static void WeedLinesCommand()
  224.   {
  225.     Document doc = Application.DocumentManager.MdiActiveDocument;
  226.     using( doc.LockDocument() )  
  227.     using( Transaction tr = doc.TransactionManager.StartTransaction() )
  228.     {
  229.       var linesToErase = doc.Database.ModelSpace()
  230.               .GetObjects<Line>()
  231.               .Where( line => line.Length < 0.1 )
  232.               .UpgradeOpen();
  233.                
  234.       foreach( Line line in linesToErase )
  235.       {
  236.         line.Erase();
  237.       }
  238.  
  239.       tr.Commit();
  240.     }
  241.   }
  242.  
  243.   // Example: Displays the sum of the areas of
  244.   // all closed Polylines in the current space.
  245.  
  246.   [CommandMethod( "LINQ_SUMPOLYLINEAREAS" )]
  247.   public static void SumPolylineAreasCommand()
  248.   {
  249.     Document doc = Application.DocumentManager.MdiActiveDocument;
  250.  
  251.     using( Transaction tr = doc.TransactionManager.StartTransaction() )
  252.     {
  253.       double area = doc.Database.CurrentSpace()
  254.         .GetObjects<Polyline>()
  255.         .Where( pline => pline.Closed )
  256.         .Sum( pline => pline.Area );
  257.        
  258.       doc.Editor.WriteMessage(
  259.         "\nTotal Area of all closed polylines: {0} units",
  260.         area
  261.       );
  262.      
  263.       tr.Commit();
  264.     }
  265.   }
  266.  
  267.   // Example: Display the handle of the circle
  268.   // in modelspace having the largest Radius
  269.   // (or the last of multiple circles having the
  270.   // largest Radius):
  271.  
  272.   [CommandMethod( "LINQ_FINDLARGESTCIRCLERADIUS" )]
  273.   public static void FindLargestCircleRadiusCommand()
  274.   {
  275.     Document doc = Application.DocumentManager.MdiActiveDocument;
  276.     using( Transaction tr = doc.TransactionManager.StartTransaction() )
  277.     {
  278.       var circles = doc.Database.ModelSpace().GetObjects<Circle>();
  279.       if( circles.Any() )
  280.       {
  281.         Circle largest =
  282.           circles.Aggregate(
  283.             (a, b) => a.Radius > b.Radius ? a : b
  284.           );
  285.  
  286.         doc.Editor.WriteMessage(
  287.           "\nLargest circle - handle: {0}, Radius: {1}",
  288.           largest.Handle.ToString(), largest.Radius
  289.         );
  290.       }
  291.       else
  292.       {
  293.         doc.Editor.WriteMessage( "\nNo Circles were found.");
  294.       }
  295.       tr.Commit();
  296.     }
  297.   }
  298. }
  299.      
  300.  
  301.  
  302.  


edit:kdub - formatting code = csharp

edit (author) - added missing generic type argument on UpgradeOpen<T>() extension method

edit (author) - added missing IsSubclassOf() extension method
« Last Edit: March 28, 2012, 12:22:30 PM by TheMaster »

gile

  • Gator
  • Posts: 2507
  • Marseille, France
Re: Iterating through block definitions by name
« Reply #5 on: March 26, 2012, 06:16:05 AM »
Thank you very much Tony for these precisions/explainations. :kewl:
Speaking English as a French Frog

kaefer

  • Guest
Re: Iterating through block definitions by name
« Reply #6 on: March 26, 2012, 06:51:44 AM »
Code: [Select]
  //   1. It is incorrect to target *any* IEnumerable, which
  //      includes things like System.String. You shouldn't
  //      allow an extension method to be invoked on a target
  //      that is not applicable or elegible.

Why ever not? It wouldn't be very different from calling a static method with the non-generic IEnumerable as parameter, and in any case, Linq's Cast<TResult>() method will take care of the error conditions and throws ArgumentNull or InvalidCast as appropriate
(n.b. this is different from F#'s Seq.cast function).

As for guidelines to extensions methods, there are the recommendations to use them sparingly and to prefer inheritance wherever possible, and that's about it.

To address the slight performance hit previously incurred by the opening of all objects, I hereby adopt your operation on RXClass. The result's still generic, working on any IEnumerable<ObjectId> and any untyped IEnumerable which IEnumerator.Current implementation is of type ObjectId.

Code - C#: [Select]
  1.     public static class IEnumExtensions
  2.     {
  3.         public static T GetObject<T>(this ObjectId id) where T : DBObject
  4.         {
  5.             return (T)id.GetObject(OpenMode.ForRead);
  6.         }
  7.         public static IEnumerable<T> GetObjects<T>(this IEnumerable ids) where T : DBObject
  8.         {
  9.             RXClass rx = RXClass.GetClass(typeof(T));
  10.             return
  11.                 from ObjectId id in ids
  12.                 where id.ObjectClass.IsDerivedFrom(rx)
  13.                 select id.GetObject<T>();
  14.         }
  15.     }
  16.     public static class IEnumExtensionsTestCommand
  17.     {
  18.         [CommandMethod("DELETESHORTLINES")]
  19.         public static void deleteShortLinesCommand()
  20.         {
  21.             Database db = HostApplicationServices.WorkingDatabase;
  22.             using (Transaction tr = db.TransactionManager.StartTransaction())
  23.             {
  24.                 var linesToErase =
  25.                     SymbolUtilityServices.GetBlockModelSpaceId(db)
  26.                         .GetObject<BlockTableRecord>()
  27.                         .GetObjects<Line>()
  28.                         .Where(line => line.Length < 0.1);
  29.                 foreach (Line line in linesToErase)
  30.                 {
  31.                     line.UpgradeOpen();
  32.                     line.Erase();
  33.                 }
  34.                 tr.Commit();
  35.             }
  36.         }
  37.     }

Code - F#: [Select]
  1. // get DBObject for read and cast to 'T, if type parameter present
  2. let getObject<'T when 'T :> DBObject> (oid: ObjectId) =
  3.     oid.GetObject OpenMode.ForRead :?> 'T
  4.  
  5. // get all objects from an untyped IEnumerable
  6. let getObjects<'T when 'T :> DBObject> : System.Collections.IEnumerable -> _ =
  7.    let rx = RXClass.GetClass typeof<'T>
  8.     Seq.cast<ObjectId>
  9.     >> Seq.filter (fun oid -> oid.ObjectClass.IsDerivedFrom rx)
  10.     >> Seq.map getObject<'T>
  11.  
  12. [<CommandMethod "DeleteShortLines">]
  13. let deleteShortLinesCommand() =
  14.    let db = HostApplicationServices.WorkingDatabase
  15.    use tr = db.TransactionManager.StartTransaction()
  16.    let linesToErase =
  17.        SymbolUtilityServices.GetBlockModelSpaceId db
  18.        |> getObject<BlockTableRecord>
  19.        |> getObjects<Line>
  20.        |> Seq.filter(fun line -> line.Length < 0.1)
  21.    for line in linesToErase do
  22.        line.UpgradeOpen()
  23.        line.Erase()
  24.    tr.Commit()

Jeff H

  • Needs a day job
  • Posts: 6144
Re: Iterating through block definitions by name
« Reply #7 on: March 26, 2012, 10:37:09 AM »
I have not read enough about them for 4.0 to know what I am talking about, but could you write an ExpressionTree and just pass some type of delegate filter?
 
 

TheMaster

  • Guest
Re: Iterating through block definitions by name
« Reply #8 on: March 26, 2012, 05:59:12 PM »

Why ever not? It wouldn't be very different from calling a static method with the non-generic IEnumerable as parameter, and in any case, Linq's Cast<TResult>() method will take care of the error conditions and throws ArgumentNull or InvalidCast as appropriate (n.b. this is different from F#'s Seq.cast function).


Well, what I said was incorrect (IMO) applies to any kind of method, not just extension methods, so I wouldn't write a static method that takes any IEnumerable as an argument either, if the method can only use certain ones.

If there's only a limited set of specific types that my method can use (all of which implement IEnumerable), then I'd write an overload for each. That's essentially what I did with GetObjects<T>(). I have a set of overloads for each type (there's not that many of them).  Cast<T> is fine except that it adds another iterator to the chain, and each iterator adds a little more overhead. The C# foreach() construct takes care of casting implicitly and the last time I checked, it was faster than adding a Cast<T>().

Quote

As for guidelines to extensions methods, there are the recommendations to use them sparingly and to prefer inheritance wherever possible, and that's about it.


Well, solving the problem through inheritance might work except that we can't use inheritance to target types we didn't create (even if they're not sealed), in cases where instances cannot be instantiated (e.g., where instances are provided by another type exclusively).

Quote

To address the slight performance hit previously incurred by the opening of all objects, I hereby adopt your operation on RXClass. The result's still generic, working on any IEnumerable<ObjectId> and any untyped IEnumerable which IEnumerator.Current implementation is of type ObjectId.


The performance hit isn't slight when you are working with BlockTableRecords that contain between 50,000 and 200,000 objects, and your code only targets a very small percentage of them. Filtering ObjectIds by their runtime class is clearly faster than opening each ObjectId and testing the type of the returned managed wrapper using the 'as' operator (or TryCast() in VB.NET), mainly because to test a managed wrapper's type, one needs a managed wrapper to start with.  :wink: To get a managed wrapper, the underlying AcDbObject must be opened (directly or through a transaction), and have a managed wrapper created for it.

Creating managed wrappers only for the purpose of filtering source objects by each managed wrapper's type has additional performance implications beyond those that are obvious and easily measured. Those additional implications are barely-tangible and not easily identifiable, mainly because they manifest in the form of latent garbage collection overhead that by its indeterminate nature, is difficult to isolate and measure.

For example, because managed wrappers have finalizers, a reference to every instance of a managed wrapper must be added to the GC's finalization queue, which has a cost in terms of both memory and processing overhead.

The additional processing overhead results from having to add and remove each managed wrapper instance to/from the GC's finalization queue. The additional memory overhead (storing a reference to each managed wrapper in the finalization queue) is roughly equivalent to each managed wrapper having one additional private instance member holding a reference to a reference type.

The extent of the advantage that testing ObjectId runtime classes has over opening each ObjectId and testing the managed wrapper's type depends largely on the relative number of matching objects in the source sequence. The advantage increases as the relative number of matching objects in the source sequence decreases, and scales with the size of the source sequence.

For example, if code needed to process only dimensions in model space, and only ~3% of all objects in model space are dimensions, the advantage will be significant, because only 3% of all objects must opened and have a managed wrapper created. If on the other hand, one has code that needs to process most of the objects in a BlockTableRecord, there is less of an advantage because most of those objects will need to be opened for processing regardless.

But in any case, feel free to adopt my code. If I have time to remove the dependencies on the other overloads of GetObjects<T>() that I use, I'll post them.
 

TheMaster

  • Guest
Re: Iterating through block definitions by name
« Reply #9 on: March 26, 2012, 06:28:55 PM »

Code - C#: [Select]
  1.  
  2.    ........
  3.                 foreach (Line line in linesToErase)
  4.                 {
  5.                     line.UpgradeOpen();
  6.                     line.Erase();
  7.                 }
  8.                 tr.Commit();
  9.  
  10.  



The problem is a bit more complicated than what your code above suggests. The reason I use the UpgradeOpen() extension method I included, is because dealing with objects on locked layers is a reality that one most contend with (especially when processing drawings that are not open in the drawing editor, or when processing blocks other than model or paper space where filtered selection sets are typically used to preclude objects on locked layers).

The underlying purpose of the various extension methods I showed is to simplify as much as possible, application-level code.

If I didn't use that extension method, the try/catch block that's in it would need to be in any code that I write to process a BlockTableRecord (because I don't want to make the assumption that it's only going to be used on the current space or in a drawing that's open in the editor).

The example usage code I posted was implemented as a command that operates on  the active document's model space, only to serve as a simple example of how those methods might be used with a BlockTableRecord, including ones in drawings that aren't open in the editor. That's why there is no use of SelectionSets and filtering. In most of the cases where I use those methods, using a filtered selection to preclude objects on locked layers is not an option, so they must be dealt with one way or  another. So, the more-simplified version of WEEDLINES you show, that uses DBObject.UpgradeOpen() in the foreach() block will work in that scenario only if you've previously-eliminated objects on locked layers, but it won't work universally in the more general scenario represented by my example.

« Last Edit: March 26, 2012, 08:58:46 PM by TheMaster »

TheMaster

  • Guest
Re: Iterating through block definitions by name
« Reply #10 on: March 26, 2012, 07:37:06 PM »
Code: [Select]

public static string[] GetBlockNamesInDrawing(Database db)
{
  using (Transaction tr = db.TransactionManager.StartTransaction())
  {
    return db.BlockTableId
      .GetObject<BlockTable>()
      .GetObjects<BlockTableRecord>()
      .Where(btr =>
        !btr.IsAnonymous &&
        !btr.IsFromExternalReference &&
        !btr.IsFromOverlayReference &&
        !btr.IsLayout &&
        !btr.IsAProxy)
      .Select(btr => btr.Name)
      .ToArray();               // <----- look here
  }
}



I try to make helper methods like the one you show
as generic as possible, to make them more reusable.
In that spirit, I would do it this way:

Code - C#: [Select]
  1.  
  2. public static class Extensions
  3. {
  4.   // By decoupling the filtering from the
  5.   // selection of the result (the block name)
  6.   // and conversion to an array, the filtering
  7.   // can be reused for other purposes:
  8.  
  9.   public static IEnumerable<BlockTableRecord> UserBlocks(
  10.                                this IEnumerable<BlockTableRecord> source )
  11.   {
  12.     return
  13.       source.Where(btr =>
  14.         !btr.IsAnonymous &&
  15.         !btr.IsFromExternalReference &&
  16.         !btr.IsFromOverlayReference &&
  17.         !btr.IsLayout &&
  18.         !btr.IsAProxy
  19.       );
  20.   }
  21.  
  22.   public static IEnumerable<BlockTableRecord> UserBlocks( this Database db )
  23.   {
  24.        return db.BlockTableId.GetObject<BlockTable>()
  25.           .GetObjects<BlockTableRecord>()
  26.           .UserBlocks();
  27.   }
  28.  
  29.   public static IEnumerable<string> UserBlockNames( this Database db )
  30.   {
  31.     return db.UserBlocks().Select( btr => btr.Name );
  32.   }
  33. }
  34.  
  35.  
  36. public static class AcDbExtensions
  37. {
  38.  
  39.     // SymbolTable  //////////////////////////////////////////////////////////
  40.     //
  41.     // The reason for having overloads of GetObject<T> for
  42.     // SymbolTable/SymbolTableRecord based types, is to
  43.     // support getting erased objects via the IncludingErased
  44.     // property. Otherwise, a more generic GetObjects<T>
  45.     // would suffice.
  46.  
  47.     // To be completely correct, a version of this method
  48.     // targeting each type derived from SymbolTable should be
  49.     // defined, but I'll leave that up to you and will just check
  50.     // the generic argument at runtime,  :grin:
  51.  
  52.   public static IEnumerable<T> GetObjects<T>( this SymbolTable source )
  53.     where T: SymbolTableRecord
  54.   {
  55.     return GetObjects<T>( source, OpenMode.ForRead, false );
  56.   }
  57.  
  58.   public static IEnumerable<T> GetObjects<T>( this SymbolTable source, OpenMode mode )
  59.     where T: SymbolTableRecord
  60.   {
  61.     return GetObjects<T>( source, mode, false );
  62.   }
  63.  
  64.   public static IEnumerable<T> GetObjects<T>(
  65.     this SymbolTable source,
  66.     OpenMode mode,
  67.     bool includingErased ) where T: SymbolTableRecord
  68.   {
  69.  
  70.     if( typeof( T ) == typeof( SymbolTableRecord ) )
  71.       throw new ArgumentException(
  72.             "Requires a concrete type derived from SymbolTableRecord");
  73.  
  74.     foreach( ObjectId id in includingErased ? source.IncludingErased : source )
  75.     {
  76.       yield return (T) id.GetObject( mode, includingErased, false );
  77.     }
  78.   }
  79.  
  80.   public static T GetObject<T>( this ObjectId id )
  81.     where T: DBObject
  82.   {
  83.        return (T) id.GetObject( OpenMode.ForRead, false, false );
  84.   }
  85. }
  86.  
  87.  

Note that I don't call ToArray() in any of these, because that
would defeat the purpose/advantage of deferred execution, and
force the entire sequence to be evaluated. For example, pretending
for the moment that the BlockTable.Has() method and this[] indexer
did not exist, and you were trying to find out if a block with a given
name exists, you can use the above methods without forcing every
block to go through the test done by the Where() call in UserBlocks():

Code - C#: [Select]
  1.  
  2.   // useless example that emulates BlockTable.Has()
  3.   // in LINQ, to demonstrate deferred execution:
  4.  
  5.   Database db = HostApplicationServices.WorkingDatabase;
  6.  
  7.   using( Transaction tr = db.TransactionManager.StartTransaction() )
  8.   {
  9.  
  10.     bool found = db.UserBlockNames().Contains( "SINK" );
  11.  
  12.     if( ! found )
  13.       Console.WriteLine("No block named SINK was found");
  14.  
  15.     tr.Commit();
  16.   }
  17.    
  18.  
   
In the above code, if a block with the given name is found,
each of the chained iterators returned by the calls to each
of the extension methods used will stop enuemerating items
as soon as the block is found. That's because we don't call
ToArray(), which would otherwise cause the LINQ expression
to be executed on every BlockTableRecord in the database,
rather than on only those appearing before the found block.

For example, if your drawing contained 250 blocks, but the
"SINK" block was the 3rd entry in the block table, there will
only need to be three calls to the .Where(...) method, and
the Select(..) method before the call to Contains() returns
true. If you instead called ToArray() on any intermediate
result, then all 250 BlockTableRecords will be passed into
250 calls to Where(...), and 250 calls to Select(...).  :wink:

So, avoid the temptation to write methods that use LINQ and
then return the result as an array of T[], rather than as an
IEnumerable<T>, because that can lead to needless execution
in scenarious like the one above (searching for a block with
a given name).

Of course, if you need all the block names in an array, you
can just as easily call ToArray() on the result of a call to the
Database.UserBlockNames() method in your application code.

The UserBlocks() extension methods I show above are similar
to versions I have here with the same name, and I use them
frequently when I want to process only those blocks that are
'user-defined'.

edit: (author): Added missing call to GetObjects<T>()
« Last Edit: March 27, 2012, 03:08:25 AM by TheMaster »

gile

  • Gator
  • Posts: 2507
  • Marseille, France
Re: Iterating through block definitions by name
« Reply #11 on: March 27, 2012, 01:53:04 AM »
Shouldn't the IEnumerable<BlockTableRecord> UserBlocks() method be:
Code - C#: [Select]
  1.         public static IEnumerable<BlockTableRecord> UserBlocks(this Database db)
  2.         {
  3.             return ((IEnumerable)db.BlockTableId
  4.               .GetObject(OpenMode.ForRead, false, false))
  5.               .Cast<ObjectId>()
  6.               .Select(id => (BlockTableRecord)id.GetObject(OpenMode.ForRead, false, false))
  7.               .UserBlocks();
  8.         }

Or, with SymbolTable extension methods:
Code - C#: [Select]
  1.         public static IEnumerable<BlockTableRecord> UserBlocks(this Database db)
  2.         {
  3.             return db.BlockTableId
  4.                 .GetObject<BlockTable>()
  5.                 .GetObjects<BlockTableRecord>()
  6.                 .UserBlocks();;
  7.         }
Speaking English as a French Frog

TheMaster

  • Guest
Re: Iterating through block definitions by name
« Reply #12 on: March 27, 2012, 02:21:24 AM »
Sorry, yes. That's what happens when I hastily try to remove dependencies from my code so I can post it in a usable form  :oops:

The working version I use calls my overload of GetObjects<T> that targets SymbolTableRecord, and when I took the call to it out of the code, i forgot to replace it with the equivalent native API calls (which is what you show). :oops:

Thanks for pointing that out

Shouldn't the IEnumerable<BlockTableRecord> UserBlocks() method be:
Code - C#: [Select]
  1.         public static IEnumerable<BlockTableRecord> UserBlocks(this Database db)
  2.         {
  3.             return ((IEnumerable)db.BlockTableId
  4.               .GetObject(OpenMode.ForRead, false, false))
  5.               .Cast<ObjectId>()
  6.               .Select(id => (BlockTableRecord)id.GetObject(OpenMode.ForRead, false, false))
  7.               .UserBlocks();
  8.         }

Or, with SymbolTable extension methods:
Code - C#: [Select]
  1.         public static IEnumerable<BlockTableRecord> UserBlocks(this Database db)
  2.         {
  3.             return db.BlockTableId
  4.                 .GetObject<BlockTable>()
  5.                 .GetObjects<BlockTableRecord>()
  6.                 .UserBlocks();;
  7.         }

TheMaster

  • Guest
Re: Iterating through block definitions by name
« Reply #13 on: March 27, 2012, 03:09:25 AM »
Ok, the previous post was updated to use the added overloads
for SymbolTable.GetObjects<T>().
« Last Edit: March 27, 2012, 05:39:55 AM by TheMaster »

TheMaster

  • Guest
Re: Iterating through block definitions by name
« Reply #14 on: March 27, 2012, 06:00:19 AM »

Code - C#: [Select]
  1.    
  2.           .Where( btr =>
  3.                !btr.IsAnonymous &&
  4.                !btr.IsFromExternalReference &&
  5.                !btr.IsFromOverlayReference &&
  6.                !btr.IsLayout &&
  7.                !btr.IsAProxy
  8.            )
  9.  

Not trying to win the nic-picking contest, but here is
one more very minor issue that I would do in a slightly-
different way:

While negation (! bool) isn't a big deal, it is very slighly
more expensive than  ! ( bool || bool || bool ).

In other words, rather than:

    Where( ! bool1 && ! bool2 && ! bool3 && ! bool4 ....)

I would instead write it as:

    Where ( ! ( bool1 || bool2 || bool3 || bool4 ...) )

So...

Code - C#: [Select]
  1.    
  2.           .Where( btr =>
  3.                ! ( btr.IsAnonymous ||
  4.                     btr.IsFromExternalReference ||
  5.                     btr.IsFromOverlayReference ||
  6.                     btr.IsLayout ||
  7.                     btr.IsAProxy )
  8.            )
  9.  

Now we're really nit-picking   :lmao: