This is one of those topics that has been at the back of my mind for a number of years. Here’s a question I received via a blog comment back in 2009:
I was wondering if there's an easy way to modify the objects to purge. For example, if a particular text style was included in the drawing that I did not want to be purged. Can this easily be done?
Here’s how I responded, at the time:
There are a couple of ways:
You can maintain your own list of objects "to keep" and remove any items that are on this list from idsToPurge before you erase them.
Or you can create an object that's owned at some level by the Database (an Xrecord placed inside the Named Objects Dictionary should do it) that contains "hard" references to the objects you wish to keep (which means using DxfCode.HardPointerId - or one of the indeces just following it - when creating your TypedValues). This has the advantage of also stopping the standard PURGE command from removing those objects.
The question and my first suggestion, above, relates to the code shown in the blog post this comment related to. It’s the second suggestion that we’re going to explore over the next couple of posts…
I probably wouldn’t have gone back and looked at this, but the topic reared its head again in relation to this recent post (and its comments). The idea related to providing a way to identify “core” (which could mean system) objects, so that your application doesn’t inadvertently delete them.
I’d considered storing a list in memory – and then exposing an extension method to check whether an id was on the list – but had then realised that making this persistent, and using Database.Purge() to perform the check, would keep this much simpler and more unified with AutoCAD. In this post we’re going to create an API to simplify this – via extension methods on the ObjectId and Transaction classes – and in tomorrow’s we’ll implement some commands to exercise it.
Here’s the C# code that allows you to manage “locking objects” in an AutoCAD drawing. These locks will prevent objects from being purged – by maintaining Xrecords containing hard-pointer references to them, stored in a custom dictionary in the drawing – and will let applications know via the ObjectId.IsErasable() extension method (until we have extension properties in C#, perhaps in C# 7) whether the object in question can safely be erased.
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.EditorInput;
using System.Collections.Generic;
namespace LockingObjects
{
public static class ObjectLocking
{
// The name for our object lock dictionary
const string dictName = "TtifObjectLocks";
// Internal objects to avoid repeated allocation
private static RXClass _cdict = null;
private static RXClass _ctab = null;
/// <summary>
/// Check whether this object can be erased safely.
/// </summary>
/// <param name="id">The ID of the object to check.</param>
/// <returns>A Boolean indicating whether other objects rely on it.</returns>
public static bool IsErasable(this ObjectId id)
{
var idc = new ObjectIdCollection(new ObjectId[] { id });
id.Database.Purge(idc);
return idc.Count > 0;
}
// The various collections in the drawing
public enum Contents
{
NOD = 1,
Blocks = 2,
DimStyles = 4,
Layers = 8,
Linetypes = 16,
RegApps = 32,
TextStyles = 64,
Ucs = 128,
Viewports = 256,
Views = 512
};
/// <summary>
/// Retrieve all the non-graphical objects in the specified drawing.
/// </summary>
/// <param name="db">The drawing Database.</param>
/// <returns>An ObjectIdCollection of the objects in the drawing.</returns>
public static ObjectIdCollection GetAllObjectsToLock(
this Transaction tr, Database db)
{
var c =
Contents.Blocks | Contents.DimStyles | Contents.Layers |
Contents.Linetypes | Contents.NOD | Contents.RegApps |
Contents.TextStyles | Contents.Ucs | Contents.Viewports |
Contents.Views;
return tr.GetObjectsToLock(db, c);
}
/// <summary>
/// Retrieve the desired objects from the specified drawing.
/// </summary>
/// <param name="db">The drawing Database.</param>
/// <param name="c">The collections to retrieve.</param>
/// <returns>An ObjectIdCollection of the objects in the drawing.</returns>
public static ObjectIdCollection GetObjectsToLock(
this Transaction tr, Database db, Contents c)
{
var ids = new ObjectIdCollection();
if ((c & Contents.NOD) > 0)
tr.GetCollectionContents(db.NamedObjectsDictionaryId, ids);
if ((c & Contents.Blocks) > 0)
tr.GetCollectionContents(db.BlockTableId, ids);
if ((c & Contents.DimStyles) > 0)
tr.GetCollectionContents(db.DimStyleTableId, ids);
if ((c & Contents.Layers) > 0)
tr.GetCollectionContents(db.LayerTableId, ids);
if ((c & Contents.Linetypes) > 0)
tr.GetCollectionContents(db.LinetypeTableId, ids);
if ((c & Contents.RegApps) > 0)
tr.GetCollectionContents(db.RegAppTableId, ids);
if ((c & Contents.TextStyles) > 0)
tr.GetCollectionContents(db.TextStyleTableId, ids);
if ((c & Contents.Ucs) > 0)
tr.GetCollectionContents(db.UcsTableId, ids);
if ((c & Contents.Viewports) > 0)
tr.GetCollectionContents(db.ViewportTableId, ids);
if ((c & Contents.Views) > 0)
tr.GetCollectionContents(db.ViewTableId, ids);
return ids;
}
/// <summary>
/// Lock the specified objects using the provided key for the object name.
/// </summary>
/// <param name="ids">The objects to lock.</param>
/// <param name="key">The name for the lock object.</param>
public static void LockObjects(
this Transaction tr, ObjectIdCollection ids, string key
)
{
if (ids.Count == 0)
return;
// Use the first ObjectId to determine the Database to use
var db = ids[0].Database;
var dict = tr.GetLockDictionary(db, OpenMode.ForRead);
if (dict != null)
{
var rb = HardPointersForIds(ids);
Xrecord xr;
if (dict.Contains(key))
{
// Update the existing lock object
xr = tr.GetObject((ObjectId)dict[key], OpenMode.ForWrite) as Xrecord;
xr.Data = rb;
}
else
{
// Create a new lock object
xr = new Xrecord();
xr.XlateReferences = true;
xr.Data = rb;
dict.UpgradeOpen();
dict.SetAt(key, xr);
tr.AddNewlyCreatedDBObject(xr, true);
}
}
}
/// <summary>
/// Remove the specified object lock for this drawing.
/// </summary>
/// <param name="db">The drawing Database.</param>
/// <param name="key">The name of the lock object to remove.</param>
public static void RemoveObjectLock(
this Transaction tr, Database db, string key
)
{
var dict = tr.GetLockDictionary(db, OpenMode.ForRead);
if (dict != null)
{
if (dict.Contains(key))
{
var entry =
tr.GetObject((ObjectId)dict[key], OpenMode.ForWrite);
entry.Erase();
}
}
}
/// <summary>
/// Get the object locks for the specified drawing.
/// </summary>
/// <param name="db">The drawing Database.</param>
/// <returns>An array of names of the lock objects in the drawing.</returns>
public static string[] GetObjectLocks(
this Transaction tr, Database db
)
{
var dict = tr.GetLockDictionary(db, OpenMode.ForRead);
if (dict == null)
return null;
var names = new List<string>(dict.Count);
foreach (var entry in dict)
{
names.Add(entry.Key);
}
return names.ToArray();
}
/// <summary>
/// Returns whether the specified object lock exists in a drawing.
/// </summary>
/// <param name="db">The drawing Database.</param>
/// <param name="key">The name of the lock object to check for.</param>
/// <returns>A Boolean indicating the existence of the lock object.</returns>
public static bool HasObjectLock(
this Transaction tr, Database db, string key
)
{
var dict = tr.GetLockDictionary(db, OpenMode.ForRead);
if (dict == null)
return false;
return dict.Contains(key);
}
// Private helpers
// Recursively extract the ObjectIds from a collection
// (could be a symbol table or a dictionary - or just a single object)
private static void GetCollectionContents(
this Transaction tr, ObjectId id, ObjectIdCollection ids
)
{
// Add the object's ID (whether a single object or a collection)
ids.Add(id);
// Only get the RXClasses the first time we check
if (_cdict == null)
{
_cdict = RXObject.GetClass(typeof(DBDictionary));
}
if (_ctab == null)
{
_ctab = RXObject.GetClass(typeof(SymbolTable));
}
// Check whether we have a collection
bool isdict = id.ObjectClass.IsDerivedFrom(_cdict);
bool istab = id.ObjectClass.IsDerivedFrom(_ctab);
if (isdict || istab)
{
// If so, open it
var obj = tr.GetObject(id, OpenMode.ForRead);
if (isdict)
{
var dict = obj as DBDictionary;
if (dict != null)
{
// Recurse on each of the dictionary's entries
foreach (var entry in dict)
{
GetCollectionContents(tr, entry.Value, ids);
}
}
}
else
{
// Also get entities from blocks?
// Not needed for the purge prevention scenario
var tab = obj as SymbolTable;
if (tab != null)
{
// Recurse on each of the symbol table records
foreach (var rec in tab)
{
GetCollectionContents(tr, rec, ids);
}
}
}
}
}
// Get the lock dictionary from the NOD (will create one if none exists)
private static DBDictionary GetLockDictionary(
this Transaction tr, Database db, OpenMode mode
)
{
DBDictionary dict = null;
var nod =
tr.GetObject(db.NamedObjectsDictionaryId, OpenMode.ForRead)
as DBDictionary;
// If we find our dictionary in the NOD, open and return it
if (nod.Contains(dictName))
{
dict = tr.GetObject((ObjectId)nod[dictName], mode) as DBDictionary;
}
else
{
// Otherwise create it, add it to the NOD, and make sure it's open
// according to the specified mode
dict = new DBDictionary();
dict.TreatElementsAsHard = true;
nod.UpgradeOpen();
nod.SetAt(dictName, dict);
tr.AddNewlyCreatedDBObject(dict, true);
nod.DowngradeOpen();
switch (mode)
{
case OpenMode.ForRead:
dict.DowngradeOpen();
break;
case OpenMode.ForNotify:
dict.DowngradeToNotify(true);
break;
default:
break;
}
}
return dict;
}
// Return a resbuf of hard pointers, one for each ObjectId provided
private static ResultBuffer HardPointersForIds(ObjectIdCollection ids)
{
var rb = new ResultBuffer();
var gc = (int)DxfCode.HardPointerId;
foreach (ObjectId id in ids)
{
rb.Add(new TypedValue(gc, id));
}
return rb;
}
}
}
A few comments on some choices made, here:
- We extend the Transaction object with the bulk of the implementation – as most developers will be using the Transaction system with .NET – but it could also hang off the Database object and (for instance) use OpenCloseTransactions internally for the drawing access.
- Some code has been included to simplify extraction of ObjectIds from the drawing: we recursively extract these from two different types of collection – DBDictionary and SymbolTable – objects. We don’t currently extract ObjectIds for entities – as these are not typically purged – but we could certainly do so.
- Applications will need to use ObjectId.IsErasable() – or the underlying Database.Purge(), if checking lots of objects at once – if they want to test erasability. Applications that don’t will still be able to erase objects but run a greater risk of drawing corruption (just as they do today, admittedly).
Incidentally, I’ve added comments that can be understood by Visual Studio’s IntelliSense to make our API more discoverable:
That’s it for this post. In the next post we’ll implement some commands that take advantage of these new methods (as well as ObjectId.IsErasable(), of course).