Breaking it down - a closer look at the C# code for importing blocks

I didn't spend as much time as would have liked talking about the code in the previous topic (it was getting late on Friday night when I posted it). Here is a breakdown of the important function calls.

The first major thing we do in the code is to declare and instantiate a new Database object. This is the object that will represent our in-memory drawing (our side database). The information in this drawing will be accessible to us, but not loaded in AutoCAD's editor.

Database sourceDb = new Database(false, true);

Very importantly, the first argument (buildDefaultDrawing) is false. You will only ever need to set this to true in two situations. If you happen to pass in true by mistake when not needed, the function will return without an error, but the DWG will almost certainly be corrupt. This comes up quite regularly, so you really need to watch for this subtle issue.

Here are the two cases where you will want to set buildDefaultDrawing to true:

  1. When you intend to create the drawing yourself, and not read it in from somewhere
  2. When you intend to read in a drawing that was created in R12 or before

Although this particular sample doesn't show the technique, if you expect to be reading pre-R13 DWGs into your application in a side database, you will need to check the DWG's version and then pass in the appropriate value into the Database constructor. Here you may very well ask, "but how do I check a DWG's version before I read it in?" Luckily, the first 6 bytes of any DWG indicate its version number (just load a DWG into Notepad and check out the initial characters):

AC1.50 = R2.05
AC1002 = R2.6
AC1004 = R9
AC1006 = R10
AC1009 = R11/R12
AC1012 = R13
AC1014 = R14
AC1015 = 2000/2000i/2002
AC1018 = 2004/2005/2006
AC1021 = 2007

You'll be able to use the file access routines of your chosen programming environment to read the first 6 characters in - AC1009 or below will require a first argument of true, otherwise you're fine with false.

Next we ask the user for the path & filename of the DWG file:

sourceFileName =

  ed.GetString("\nEnter the name of the source drawing: ");

Nothing very interesting here, other than the fact I've chosen not to check whether the file actually exists (or even whether the user entered anything). The reason is simple enough: the next function call (to ReadDwgFile()) will throw an exception if the file doesn't exist, and the try-catch block will pick this up and report it to the user. We could, for example, check for that particular failure and print a more elegant message than "Error during copy: eFileNotFound", but frankly that's just cosmetic - the exception is caught and handled well enough.

sourceDb.ReadDwgFile(sourceFileName.StringResult,

                     System.IO.FileShare.Read,

                     true,

                     "");

This is the function call that reads in our drawing into the side database. We pass in the results of the GetString() call into the filename argument, specifying we're just reading the file (for the purposes of file-locking: this simply means that other applications will be able to read the DWG at the same time as ours but not write to it). We then specify that we wish AutoCAD to attempt silent conversion of DWGs using a code-page (a pre-Unicode concept related to localized text) that is different to the one used by the OS we're running on. The last argument specifies a blank password (we're assuming the drawing being opened is either not password protected or its password has already been entered into the session's password cache).

Next we instantiate a collection object to store the IDs of all the blocks we wish to copy across from the side database to the active one:

ObjectIdCollection blockIds = new ObjectIdCollection();

We then create a transaction which will allow us to access interesting parts of the DWG (this is the recommended way to access DWG content in .NET). Using the transaction we open the block table of the side database for read access, specifying that we only wish to access it if it has not been erased:

BlockTable bt =

    (BlockTable)tm.GetObject(sourceDb.BlockTableId,

                             OpenMode.ForRead,

                             false);

From here - and this is one of the beauties of using the managed API to AutoCAD - we simply use a standard foreach loop to check each of the block definitions (or "block table records" in AcDb parlance).

foreach (ObjectId btrId in bt)

{

  BlockTableRecord btr =

    (BlockTableRecord)tm.GetObject(btrId,

                                   OpenMode.ForRead,

                                   false);

  // Only add named & non-layout blocks to the copy list

  if (!btr.IsAnonymous && !btr.IsLayout)

    blockIds.Add(btrId);

  btr.Dispose();

}

This code simply opens each block definition and only adds its ID to the list to copy if it is neither anonymous nor a layout (modelspace and each of the paperspaces are stored in DWGs as block definitions - we do not want to copy them across). We also call Dispose() on each block definition once we're done (this is a very good habit to get into).

And finally, here's the function call that does the real work:

sourceDb.WblockCloneObjects(blockIds,

                            destDb.BlockTableId,

                            mapping,

                            DuplicateRecordCloning.Replace,

                            false);

WblockCloneObjects() takes a list of objects and attempts to clone them across databases - we specify the owner to be the block table of the target database, and that should any of the blocks we're copying (i.e. their names) already exist in the target database, then they should be overwritten ("Replace"). You could also specify that the copy should not happen for these pre-existing blocks ("Ignore").

29 responses to “Breaking it down - a closer look at the C# code for importing blocks”

  1. thx Kean,
    Great update, very helpful. Keep them coming, I'm learning a lot them these types of posts. Now I need to figure out how to update a specific block from the side DB. Anything I should look out for?

  2. Hi Barry,

    If you have the name of the block you can avoid the foreach loop, and simply use:

    BlockTableRecord btr =
    (BlockTableRecord)tm.GetObject(bt[blockName],
    OpenMode.ForRead);

    Then you just have to add the block definitions objectID into the list:

    blockIds.Add(btr.ObjectId);

    And that's it! 🙂

    Kean

  3. Hi,Kean
    I can use the DwgVersion Enum to get the correct AutoCAD version for 2000~2007,but DwgVersion.AC1014 throws an exception.
    Can you help me?
    Thanks in advance.

  4. Hi csharpbird,

    What exactly are you trying to do?

    Regards,

    Kean

  5. Hi,Kean
    The code is like this:
    Database db=new Database(flase,true);
    ....
    db.SaveAs("test.dwg",DwgVersion.AC1014);

    But it will throw an error(saveas 2000/2004/2007 is OK).

  6. Hi csharpbird,

    A couple of things:

    If you're creating a DWG file to save out, you should pass in "True" as the first paramater to the Database constructor. But that won't help with the exception.

    It seems SaveAs only supports 2000, 2004 and 2007 DWG formats (you can check this by calling db.GetSupportedSaveVersions()). Which basically means the function only makes calls to acdbSaveAsR2000/R2004 (ObjectARX functions) for previous DWG versions.

    You should, however, be able to P/Invoke acdbSaveAsR14() directly, passing db.UnmanagedObject as the first parameter.

    Regards,

    Kean

  7. Hi,Kean
    [DllImport("acad.exe", EntryPoint = "?acdbSaveAsR14@@YA?AW4ErrorStatus@Acad@@PAVAcDbDatabase@@PB_W@Z", CharSet = CharSet.Auto)]
    static extern bool acdbSaveAsR14(IntPtr pDb, string fileName);
    public static void SaveDatabaseAsR14(Database db,string fileName)
    {
    acdbSaveAsR14(db.UnmanagedObject, fileName);
    }
    Database db = HostApplicationServices.WorkingDatabase;
    SaveDatabaseAsR14(db, "C:\\test.dwg");

    But it throws the following exception:
    Unable to find an entry point named '?' in DLL 'acad.exe'.

  8. Hi csharpbird,

    Have you tried looking in acdb17.dll instead?

  9. Is there a way to see whether a .dwg is drawn in Mechanical or pure Autocad by opening it with Notepad or a text editor?

  10. No - you would need to scan for Mechanical objects with RealDWG (if not inside AutoCAD or AutoCAD Mechanical).

    Kean

  11. Hi Kean! I'm trying to figure out if it's possible to exchange blocks between remote hosts using SOAP. Here you shown that is possible to import blocks between databases on the same host, but is possible to do the same thing on a network (SOAP)????
    I wasn't able to find an answer!!!
    Can you help me??

  12. I can see a few ways to do this... the choice will depend on a few things - how much effort you want to go to, availability of bandwidth, etc.

    I would either encapsulate the DWG as a binary chunk (blob) in the SOAP message, just send an accessible URL for the file, or parse the DWG and extract XML content respresenting the important stuff in the DWG.

    But I haven't attempted any of these myself.

    Kean

  13. Good morning Kean.

    Can I use the same code to scan the side database for system variables? Say if I wanted to see if USERR1 had a value assigned to it, could I read in the drawing and scan for that or other drawing variables?

    Thanks for your help.

    Jason

  14. Hi Jason,

    Sure thing: all the drawing-level system variables are exposed as properties off the Database object. So you could check (or set) the value of sourceDb.Userr1, for instance.

    Regards,

    Kean

  15. Hi Kean,
    I'm trying to create a new Layout in my current database from a template database. I'm have an issue getting the layoutmanager to read the template database. Can you give me a direction to take that might help me with this task. It seems like it would be simular to moving blocks from one database to another.
    Thank you. Randy

  16. Hmm... there may be more to it than simply copying the block (or its contents) across: you may need to create or copy an object in the Layout dictionary (or something else I'm missing).

    I suggest posting your question to the AutoCAD .NET Discussion Group or via the ADN website, if you're a member.

    Kean

  17. These method of importing only imports the block reference and not the eventually attached attributes. How would you do it to copy them too?
    Looking further I discovered the attribute was imported, but works only after attsync, is there a way to do ATTSYNC in code?

  18. Alex -

    The code should import (and replace) block definitions: if these redefine blocks with different attributes, more work will, of course, be needed.

    ATTSYNC clearly does this: it opens each block reference and checks it against its block definition, and adds/removes attribute references, as needed.

    This is not covered by this post, as you've found out - I'll add it to my list of topics to come back to, but I can't make any guarantees. Ths above pointers should help, at least.

    Regards,

    Kean

  19. I have a nasty problem, somewhat related to this post, which I can not find an answer for anywhere. I have a function which imports blocks in the manner described in the post, and it all works fine, but I also create a great many blocks from scratch in my code. I have had operational code which does this for years now, but I am trying to switch all of my references to the Managed classes instead of the COM libraries. When I create a block with attributes, then insert the block through code, then attempt to set the values of some of the attributes, they aren't there until I attsync. Since I can't find a way to duplicate attsync in the managed classes, I tried using SendCommandtoExecute, (which I hate doing...) and it still doesn't work because it doesn't execute the command string until after my code ended and autocad resumes control. Some behavior is even wierder.. Like if you execute my code to create a block, insert it, and set attributes, without the attsync, you get blocks in modelspace which don't think they have attributes, but If you then insert one of the same blocks manually, it has attributes, without running attsync. If you run the command again using the same blocks, the "code inserted" blocks still don't think they have attributes even though the manually inserted block is fine. If you add the attsync into the code, the "code Inserted" first round of blocks will have attributes which do NOT have the proper values set because the attsync didn't run until after I attempted to set the values. Then run the command again which will insert another set of the same blocks (without recreating them) and they act like they have no attributes, but the blocks have already been attsynced. again, inserting one of the same blocks manually works normally, but in the code it does not work.

    I have trimmed out a class to demonstrate the problem if you would like to see it. (BTW, I'm using VB in VS2005, .NET framework 2.0, and just recently switched to referencing AutoCAD 2009.)

  20. You're probably not adding AttributeReferences properly. Search this blog for "AttributeReference" and you'll find a number of examples...

    Kean

  21. It never would have occurred to me to do that.... Never had to do that with the COM interface or with LSP or VBA which I have also used extensively.

    If BlkDef.HasAttributeDefinitions Then
    For Each EntId In BlkDef
    SubEnt = Trans.GetObject(EntId, OpenMode.ForRead, False, True)
    If TypeOf SubEnt Is AttributeDefinition Then
    AtDef = SubEnt
    AttRef = New AttributeReference
    AttRef.SetAttributeFromBlock(AtDef, Blk2Ins.BlockTransform)
    Blk2Ins.AttributeCollection.AppendAttribute(AttRef)
    Trans.AddNewlyCreatedDBObject(AttRef, True)
    End If
    Next
    End If
    Transman.QueueForGraphicsFlush()

    Thanks for the help, that would have driven me crazy, and we have a web demonstration of this program scheduled for next wednesday. Obviously it would have been bad if I had broken it four days before.

  22. How about if I want to copy a layout and it's contens from another drawing? Not from the modelspace fo course, but the entities on the layout (The same thing as the -LAYOUT command with the Template option.)

    I have tried using the same WblockClone in a similiar manner, and then using the AddtoLayoutsDictionary or/and setting the OwnerId of the Layout to the target databases BlockTableRecord (Paper_Space#), but for some reason an exception will be thrown or at least audit will tell that the layout source was erased etc.

    Do you know how to do that? Do I need to copy a layout on the current dwg (database) first and then delete it's contents and after that copy the objects from an external database?

  23. You probably need to create the Layout first, using the LayoutManager... there's some initialization that needs to happen with a Layout before it's populated.

    You'd then normally use Database.Insert() to bring in the entities, if you were copying to modelspace. You may still want to, but use the version of the method that creates a new BTR, from which you can move the contents across to your paperspace layout.

    Good luck!

    Kean

  24. Thanks! I guess I was feeling lucky today...I got it now. Here are the steps:

    1) Create a new layout (using the layoutmanager)
    2) Clone the layout from the source database. (This is to set the paper size correct etc.)
    3) WblockClone the source layout objects
    4) Set up the Canonical media name
    5) Set the layout as the current one using the layout manager. (Otherwise it will not be visible at all before cliking any other layout tabs...)

    This way the audit will not report any erros and the newly created layout is visible.

    Mika

  25. Hi Kean,

    I was at the lecture on the F # in Las Vegas, Thanks, it was great!

    I have a question about Multi View Blocks.
    I want to import a multiviewblock from a "style file" to a drawing. Can I use WblockClone for this purpose?

    Regards,
    Lars

  26. Kean Walmsley Avatar

    Hi Lars,

    Thanks for the feedback! 🙂

    It *should* work, as the cloning mechanism will follow an references/dependencies specified by the MvBlock object, but I have to admit I haven't tried it myself.

    Cheers,

    Kean

  27. Hi Kean,

    I'm using RealDWG 2010 API to read the Dwg files (ver above 14 and below 2010). ReadDwgFile call works well most of the time. However, for some DWG files program exits without any exception while reading the drawing file.
    When I checked the same DWG file by opening it in Map 3d 2010, it opens with out any error.

    How to resolve this issue, please advice…

    try
    {
    using (objAcdb = new Database(false,true))
    {
    //Open Drawing File
    objAcdb.ReadDwgFile(m_DrawingFile, FileShare.Read, true, "");

    .....
    ....
    }
    }
    catch (Autodesk.AutoCAD.Runtime.Exception ex)
    {
    // Print message
    }
    catch (System.Exception ex)
    {
    // Print message
    }
    finally
    {
    //dispose all objects..
    }

    Regards,

    Pradeep Zambare

  28. Hi Pradeep,

    Sorry - I can't say what's wrong, off the top of my head.

    If you're an ADN member I suggest submitting a sample project with problematic drawings via the ADN site.

    Regards,

    Kean

  29. This is a shot in the dark based on the age of this post, but is there a chance that the code you (Mika) wrote to accomplish this task is available to look at? I am trying to accomplish the same thing as you and it would be great if I had something to go by that works.

Leave a Reply to Jason Cancel reply

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