Creating AutoCAD Xrefs as overlays with relative paths using .NET

After showing how to automatically attach xrefs at the origin inside AutoCAD, and then redoing the approach to take care of different unit systems, I then had the request from a couple of places to look at making the xrefs overlays and adjusting their paths to be relative rather than absolute.

Looking around, I found some code on the AutoCAD DevBlog that changes an attachment to an overlay, after the fact. Henrik Ericson found the code didn't work for him, but did spot the db.OverlayXref() method which did. So I went ahead and made use of that for overlays. I factored out the logic into a single helper method which I now call from two commands: XAO for attachments and XOO for overlays.

For relative paths, I ended up making a couple of extension methods based on this DevBlog post and this one on Stack Overflow. I also threw in a simple helper class that makes sure an object is upgraded to being open for write for the duration of its existence. I could also have required the object to be open for write in the first place, but I felt like seeing if the approach worked.

Here's the updated code with the XAO and XOO commands. Both now use relative paths by default.

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Runtime;

using System;

using System.IO;

 

namespace XrefAttachAtZero

{

  // Simple helper to make sure an object is "open for write", downgrading

  // its open status when finished. It's intended to be used in the same way

  // as a document lock from a using() statement.

 

  public class Upgrader : IDisposable

  {

    private bool _upgraded;

    private DBObject _object;

 

    public Upgrader(DBObject o)

    {

      _object = o;

      _upgraded = o.IsReadEnabled;

      if (_upgraded)

        o.UpgradeOpen();

    }

 

    public void Dispose()

    {

      Dispose(true);

      GC.SuppressFinalize(this);

    }

 

    protected virtual void Dispose(bool disposing)

    {

      if (disposing)

      {

        if (_upgraded && _object != null)

          _object.DowngradeOpen();

        _object = null;

      }

    }

  }

 

  public static class Extensions

  {

    /// <summary>

    /// Generates a relative path from one string to another.

    /// </summary>

    /// <param name="to">The path to which to return a relative path.</param>

    /// <returns>The relative path between this one and the destination.</returns>

 

    public static string RelativePathTo(this string from, string to)

    {

      var fromUri = new Uri(from);

     
var toUri = new Uri(to);

 

      // Convert to URIs for the sake of the relative path determination

 

      var relUri = fromUri.MakeRelativeUri(toUri);

      var relPath = Uri.UnescapeDataString(relUri.ToString());

 

      // Then change back

 

      return relPath.Replace('/', Path.DirectorySeparatorChar);

    }

 

    /// <summary>

    /// Changes the path of an xref's block definition to have a relative path.

    /// </summary>

    /// <param name="root">Path from which to create the relative path.</param>

    /// <returns>Whether the path was changed - only fails for non-xrefs.</returns>

 

    public static bool ChangePathToRelative(

      this BlockTableRecord btr, string root

    )

    {

      var ret = false;

      if (btr.IsFromExternalReference)

      {

        using (new Upgrader(btr))

        {

          btr.PathName = root.RelativePathTo(btr.PathName);

          ret = true;

        }

      }

      return ret;

    }

 

    /// <summary>

    /// Attaches the specified Xref to the current space in the current drawing.

    /// </summary>

    /// <param name="path">Path to the drawing file to attach as an Xref.</param>

    /// <param name="pos">Position of Xref in WCS coordinates.</param>

    /// <param name="name">Optional name for the Xref.</param>

    /// <returns>Whether the attach operation succeeded.</returns>

 

    public static bool XrefAttachAndInsert(

      this Database db, string path, Point3d pos,

      string name = null, bool overlay = false

    )

    {

      var ret = false;

      if (!File.Exists(path))

        return ret;

 

      if (String.IsNullOrEmpty(name))

        name = Path.GetFileNameWithoutExtension(path);

 

      // We'll collect any xref definitions that need reloading after our

      // transaction (there should be at most one)

 

      var xIds = new ObjectIdColl
ection
();

 

      try

      {

        using (var tr = db.TransactionManager.StartOpenCloseTransaction())

        {

          // Attach or overlay the Xref - add it to the database's block table

 

          var xId =

            overlay ? db.OverlayXref(path, name) : db.AttachXref(path, name);

          if (xId.IsValid)

          {

            // Open the newly created block, so we can get its units

 

            var xbtr = (BlockTableRecord)tr.GetObject(xId, OpenMode.ForRead);

 

            // Get the path of the current drawing

 

            var loc = Path.GetDirectoryName(db.Filename);

 

            if (xbtr.ChangePathToRelative(loc))

            {

              xIds.Add(xId);

            }

 

            // Determine the unit conversion between the xref and the target

            // database

 

            var sf = UnitsConverter.GetConversionFactor(xbtr.Units, db.Insunits);

 

            // Create the block reference and scale it accordingly

 

            var br = new BlockReference(pos, xId);

            br.ScaleFactors = new Scale3d(sf);

 

            // Add the block reference to the current space and the transaction

 

            var btr =

              (BlockTableRecord)tr.GetObject(

                db.CurrentSpaceId, OpenMode.ForWrite

              );

            btr.AppendEntity(br);

            tr.AddNewlyCreatedDBObject(br, true);

 

            ret = true;

          }

          tr.Commit();

        }

 

        // If we modified our xref's path, let's reload it

 

        if (xIds.Count > 0)

        {

          db.ReloadXrefs(xIds);

        }

      }

      catch (Autodesk.AutoCAD.Runtime.Exception)

      { }

 

      return ret;

    }

  }

 

  public static class Commands

  {

    [CommandMethod("XAO")]

    public static void XrefAttachAtOrigin()

    {

      XrefAttachOrOverlayAtOrigin();

    }

 

    [Comm
andMethod
("XOO")]

    public static void XrefOverlayAtOrigin()

    {

      XrefAttachOrOverlayAtOrigin(true);

    }

 

    private static void XrefAttachOrOverlayAtOrigin(bool overlay = false)

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

      var db = doc.Database;

      var ed = doc.Editor;

 

      // Ask the user to specify a file to attach

 

      var opts = new PromptOpenFileOptions("Select Reference File");

      opts.Filter = "Drawing (*.dwg)|*.dwg";

      var pr = ed.GetFileNameForOpen(opts);

 

      if (pr.Status == PromptStatus.OK)

      {

        // Overlay the specified file and insert it at the origin

 

        var res =

          db.XrefAttachAndInsert(

            pr.StringResult, Point3d.Origin, null, overlay

          );

 

        ed.WriteMessage(

          "External reference {0}{1} at the origin.",

          res ? "" : "not ",

          overlay ? "overlaid" : "attached"

        );

      }

    }

  }

}

 

Here's the XOO command in action. We check the created xref using the XOO command, right afterwards.

Overlay xref with relative path

I like the way the code has evolved during this series from something almost ridiculously simplistic to being much more useable. Please post a comment if you see something else that needs adding!

  1. IDisposable is a great tool, nice to see it here. Is there a reason you implemented it Explicitly? The Using pattern covers most use cases, but could there ever be a situation where the Upgrader would be a property or field that gets disposed by some other owning class? Having a publicly accessible Dispose method in that case would allow for this type of pattern. I'd recommend also to check your object/s for null in the Dispose method as a habit when implementing IDisposable.

    Awesome work as usual, I'll have to remember that technique for relative paths.

    1. Ah - I had meant to look more closely at that before posting (thanks for the prod). I ran FxCop against the assembly and found I needed to fix it... I've updated the code, also adding the check for null.

      Thanks!

      Kean

  2. Hi Kevin,

    It's off topic, but interesting to discuss. Here are some thoughts...

    If you're consistently using transactions - whether standard or "open-close" transactions - to access drawing objects, this shouldn't really happen. That said, from looking at your code there are a few pointers I can give.

    Based on the name of the function, I'm assuming this is a low-level helper function that makes sure a block has some text values stored in its extension dictionary. And that this could be called repeatedly.

    As such, I'd personally avoid all mention of document locking in a function at this level: if it's being called in a context where this is needed, you should probably perform the lock operation at a higher level in the code.

    It's also important to see the context in which this code is being called. Is it called from a reactor of some kind? It's quite possible you need to shift it to (for instance) the command boundary, to make sure some other operation doesn't have the block table record open when you call it.

    I hope this helps,

    Kean

    1. Kean,

      Thank you for taking the time to respond.

      The context in which the SetBlockExtensionDictionaryTextEntries method is called initiates when the Document.ImpliedSelectionChanged event fires. I believe my plugin is rather unusual in the way it is initiated and integrated. The code in question does not run within the context of a command. The plugin is initialized by a command so that users can control if the plugin is active, but after initialization most code is executed outside the context of a command.

      I exclusively use events exposed by the AutoCAD .Net API and do not utilize Reactors. My limited understanding is that events largely replace Reactors when working with the .Net API. I have never worked with the C++ API or VL or VBA in AutoCAD.

      The problem I have is that once the Model Space BTR is locked for read, this lock is never released. The only way to eliminate the lock that I am aware of is to close/open the drawing. I always use transactions (OpenClose) when reading or writing to the Drawing Database. Any of my code which interacts with the drawing database is invoked on the main thread.

      In the past, I have utilized the Editor.EnteringQuiescentState event to avoid certain locking errors. But I have only needed to resort to this when detecting changes to BlockReferences using the BlockReference.Modified event.

      At this point I realize I have asked too much of you. This response is long enough without me explaining how my plugin works and why. I will have to find a way to supply ADN with the source code for a plugin which can be used to reproduce the issue, which has proven difficult because I cannot send them my solution as it is and the lock occurs unpredictably if at all depending on the client machine configuration.

      Thank you again for everything,

      Kevin

      1. Kevin,

        Events are exposed via a .NET wrapper around C++ reactors: they're basically equivalent terms, for all intents and purposes.

        Rather than trying to do this kind of operation every time an entity is picked (especially as it involves locking the document), I recommend waiting until CommandEnded() or DocumentLockModeChanged().

        Regards,

        Kean

        1. Kean,

          I will try to use CommandEnded or DocumentLockModeChanged as you suggest. I already subscribe to CommandEnded as I use it to detect when one of my blocks is stretched (GP_STRETCH). In my plugin, all blocks are dynamic. These blocks are authored by content creators who leverage my system.

          Briefly, in order to enforce complex engineering rules, which already exist in a plugin system I created and are maintained by another team, the properties of our dynamic blocks are not visible in the AutoCAD Properties tool (PalletSet?). I created my own property editor PalletSet, which must be used when editing the properties of our dynamic blocks. This allows me to support things like python scripting and to leverage the existing assembly model rules to constrain the property values available. Maybe there was a better way to accomplish these requirements, but starting out in early 2012 on this project I knew nothing about AutoCAD's API, so I just found something that worked. Creating and maintaining dynamic blocks which enforce these constraints just wasn't practical. To give you an idea of the scale of the project, there are currently more than 160 dynamic blocks, each of which have many properties. I only describe all of this because I think you might find it interesting.

          Anyway, when the selection set changes, I execute code which initializes this control. When multiple blocks are selected, the available properties are filtered and grouped much the same as the standard properties tool. I'll report back if I manage to resolve this issue and/or use your suggestions.

          Best Regards,
          Kevin

Leave a Reply

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