An interesting query came into my inbox, last week. A member of one of our internal product teams was looking to programmatically modify the contents of an external reference file. She was using the code in this helpful DevBlog post, but was running into issues. She was using WblockCloneObjects() to copy a block definition across from a separate drawing into a particular xref, but found some strange behaviour.
In this post I’m going to show the steps we ended up following to make this work. We’re going to implement a slightly different scenario, where we modify an external reference to load a linetype and apply that to all the circles in its modelspace.
The application is split into two separate commands: our CRX command (for CReate Xref… yes, I know this name’s a bit confusing) will create an external drawing file – consisting of a circle with a polyline boundary around it – and reference it in the current drawing. There’s nothing very special about this command: we just create an external Database, save it to disk, and then run the standard ATTACH command via ed.Command().
The second command is called CHX (for CHange Xref) and this is the more interesting one: it attempts to edit any external reference found in modelspace, manipulating each one to have all its circles given the “dashed” linetype. If that linetype isn’t loaded it’ll go ahead and load it.
Here are the two commands in action:
The main “trick” we had to resolve when using WblockCloneObjects() was to set the WorkingDatabase to be that of the xref. This step wasn’t needed for loading linetypes, but I’ve left the code commented out for setting the WorkingDatabase appropriately: you may well need to uncomment it for your own more complex operations.
The other important step was to check whether the xref’s underlying file is accessible for write: if the drawing is open in the editor or is read-only on disk (unlikely in our scenario, as we’re creating it) then the XrefFileLock.LockFile() operation will throw an exception (but also keep a file lock in memory which will throw another exception when eventually finalized… not good). So we need to preempt the exception, avoiding the conditions under which it would be thrown.
We do this using this approach to check whether the file is in use, extended to also detect whether the file is read-only on disk. It can be extended with additional file access exceptions, if the two that I’ve included don’t end up covering the various scenarios.
Here’s the C# code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using System.IO;
using System;
namespace XrefManipulation
{
public class Commands
{
const string xloc = @"c:\temp\xref.dwg";
[CommandMethod("CRX")]
public void CreateXref()
{
var doc = Application.DocumentManager.MdiActiveDocument;
if (doc == null)
return;
var ed = doc.Editor;
// Create our Database with the default dictionaries and
// symbol tables
using (var db = new Database(true, true))
{
using (var tr = db.TransactionManager.StartTransaction())
{
// We'll add entities to its modelspace
var bt =
(BlockTable)tr.GetObject(
db.BlockTableId, OpenMode.ForRead
);
var ms =
(BlockTableRecord)tr.GetObject(
bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite
);
// Create a Circle inside a Polyline boundary
var c = new Circle(Point3d.Origin, Vector3d.ZAxis, 1.0);
ms.AppendEntity(c);
tr.AddNewlyCreatedDBObject(c, true);
var p = new Polyline(4);
p.AddVertexAt(0, new Point2d(-1, -1), 0, 0, 0);
p.AddVertexAt(1, new Point2d(-1, 1), 0, 0, 0);
p.AddVertexAt(2, new Point2d(1, 1), 0, 0, 0);
p.AddVertexAt(3, new Point2d(1, -1), 0, 0, 0);
p.Closed = true;
ms.AppendEntity(p);
tr.AddNewlyCreatedDBObject(p, true);
tr.Commit();
}
// We're going to save our file in the specified location,
// after erasing the file if it already exists
if (File.Exists(xloc))
{
try
{
File.Delete(xloc);
}
catch (System.Exception ex)
{
if (
ex is IOException || ex is UnauthorizedAccessException
)
{
ed.WriteMessage(
"\nUnable to erase existing reference file. " +
"It may be open in the editor or read-only."
);
return;
}
throw;
}
}
db.SaveAs(xloc, DwgVersion.Current);
}
// The simplest way to attach the xref is via a command
ed.Command("_.-ATTACH", xloc, "_A", "25,12.5,0", 5, 5, 0);
}
[CommandMethod("CHX")]
public void ChangeXref()
{
var doc = Application.DocumentManager.MdiActiveDocument;
if (doc == null)
return;
var ed = doc.Editor;
var db = doc.Database;
// Get the database associated with each xref in the
// drawing and change all of its circles to be dashed
using (var tr = db.TransactionManager.StartTransaction())
{
var bt =
(BlockTable)tr.GetObject(
db.BlockTableId, OpenMode.ForRead
);
var ms =
(BlockTableRecord)tr.GetObject(
bt[BlockTableRecord.ModelSpace], OpenMode.ForRead
);
// Loop through the contents of the modelspace
foreach (var id in ms)
{
// We only care about BlockReferences
var br =
tr.GetObject(id, OpenMode.ForRead) as BlockReference;
if (br != null)
{
// Check whether the associated BlockTableRecord is
// an external reference
var bd =
(BlockTableRecord)tr.GetObject(
br.BlockTableRecord, OpenMode.ForRead
);
if (bd.IsFromExternalReference)
{
// If so, get its Database and call the function
// to change the linetype of its Circles
var xdb = bd.GetXrefDatabase(false);
if (xdb != null)
{
// Lock the xref database
if (
IsFileLockedOrReadOnly(new FileInfo(xdb.Filename))
)
{
ed.WriteMessage(
"\nUnable to modify the external reference. " +
"It may be open in the editor or read-only."
);
}
else
{
using (
var xf = XrefFileLock.LockFile(xdb.XrefBlockId)
)
{
// Make sure the original symbols are loaded
xdb.RestoreOriginalXrefSymbols();
// Depending on the operation you're performing,
// you may need to set the WorkingDatabase to
// be that of the Xref
//HostApplicationServices.WorkingDatabase = xdb;
ChangeEntityLinetype(
xdb, typeof(Circle), "DASHED"
);
// And then set things back, afterwards
//HostApplicationServices.WorkingDatabase = db;
xdb.RestoreForwardingXrefSymbols();
}
}
}
}
}
}
tr.Commit();
}
}
internal virtual bool IsFileLockedOrReadOnly(FileInfo fi)
{
FileStream fs = null;
try
{
fs =
fi.Open(
FileMode.Open, FileAccess.ReadWrite, FileShare.None
);
}
catch (System.Exception ex)
{
if (ex is IOException || ex is UnauthorizedAccessException)
{
return true;
}
throw;
}
finally
{
if (fs != null)
fs.Close();
}
// File is accessible
return false;
}
// Change all the entities of the specified type in a Database
// to the specified linetype
private void ChangeEntityLinetype(
Database db, System.Type t, string ltname
)
{
using (
var tr = db.TransactionManager.StartTransaction()
)
{
// Check whether the specified linetype is already in the
// specified Database
var lt =
(LinetypeTable)tr.GetObject(
db.LinetypeTableId, OpenMode.ForRead
);
if (!lt.Has(ltname))
{
// If not, load it from acad.lin
string ltpath =
HostApplicationServices.Current.FindFile(
"acad.lin", db, FindFileHint.Default
);
db.LoadLineTypeFile(ltname, ltpath);
}
// Now go through and look for entities of the specified
// class, changing each of their linetypes
var bt =
(BlockTable)tr.GetObject(
db.BlockTableId, OpenMode.ForRead
);
var ms =
(BlockTableRecord)tr.GetObject(
bt[BlockTableRecord.ModelSpace],
OpenMode.ForRead
);
// Loop through the modelspace
foreach (var id in ms)
{
// Get each entity
var ent = tr.GetObject(id, OpenMode.ForRead) as Entity;
if (ent != null)
{
// Check its type against the one specified
var et = ent.GetType();
if (et == t || et.IsSubclassOf(t))
{
// In the case of a match, change the linetype
ent.UpgradeOpen();
ent.Linetype = ltname;
}
}
}
tr.Commit();
}
}
}
}
This technique is really useful: I can imagine lots of scenarios where in-place xref modification could be used to develop a valuable application feature. If you agree and have a particular scenario you think is worth sharing – or perhaps would like to see some sample code developed for – please post a comment!