Unlocking layers for certain AutoCAD commands using .NET
This question came up during last week's accelerator, and is part of the reason I spent creating the last post: is it possible to selectively unlock certain layers for the duration of commands that have been specified by the user? Let's take an example: you have layers that should remain locked, apart from when using the MOVE command (COPY and ERASE should not work).
The approach I took was to maintain a dictionary mapping command names to lists of layers to unlock. When a command is launched – which we can tell using the Document.CommandWillStart event – we check whether it has layers associated with it. If so, we unlock them for the duration of the command and lock them again on command completion.
I knew we'd want a map per document, so I went and used the approach from this post. Something I think people will find useful: rather than maintaining separate code to check for documents being created and closed, I used the "doc data" class itself to attach the command-tracking event handlers to documents when they're opened, and its "dispose" method to remove them when closed. This makes the code much simpler, I'm sure you'll agree.
Here's the C# code that implements the UFC command, as well as having the LL and UL commands (that use the same, slightly modified extension method) we saw last time.
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
[assembly: PerDocumentClass(typeof(LayerManipulation.UnlockCommands))]
namespace LayerManipulation
{
public class UnlockCommands : System.IDisposable
{
public const string RecordName = "TtifLayerUnlockData";
// Map commands to layers to unlock and relock
public Dictionary<string, ObjectIdCollection> CommandMap =
new Dictionary<string, ObjectIdCollection>();
private Document _doc = null;
private bool _disposed = false;
// Constructor
public UnlockCommands(Document doc)
{
_doc = doc;
doc.UserData.Add(RecordName, this);
// Add our command handlers
doc.CommandWillStart += OnCommandWillStart;
doc.CommandEnded += OnCommandEnded;
doc.CommandCancelled += OnCommandEnded;
doc.CommandFailed += OnCommandEnded;
}
void OnCommandWillStart(object sender, CommandEventArgs e)
{
var doc = (Document)sender;
var uc = doc.UserData[UnlockCommands.RecordName] as UnlockCommands;
// If the command ending is in our list, unlock the layer(s)
if (uc != null && uc.CommandMap.ContainsKey(e.GlobalCommandName))
{
doc.LockOrUnlockLayers(false, uc.CommandMap[e.GlobalCommandName], false);
}
}
void OnCommandEnded(object sender, CommandEventArgs e)
{
var doc = (Document)sender;
var uc = doc.UserData[UnlockCommands.RecordName] as UnlockCommands;
// If the command ending is in our list, relock the layer(s)
if (uc != null && uc.CommandMap.ContainsKey(e.GlobalCommandName))
{
doc.LockOrUnlockLayers(true, uc.CommandMap[e.GlobalCommandName], false);
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
// Remove our command handlers
_doc.CommandWillStart -= OnCommandWillStart;
_doc.CommandEnded -= OnCommandEnded;
_doc.CommandCancelled -= OnCommandEnded;
_doc.CommandFailed += OnCommandEnded;
_disposed = true;
}
}
public static class Extensions
{
public static void LockOrUnlockLayers(
this Document doc, bool dolock,
ObjectIdCollection layers = null, bool ignoreCurrent = true,
bool lockZero = false
)
{
var db = doc.Database;
var ed = doc.Editor;
using (var tr = db.TransactionManager.StartTransaction())
{
var layerIds =
(layers != null ? (IEnumerable)layers :
(IEnumerable)tr.GetObject(db.LayerTableId, OpenMode.ForRead));
foreach (ObjectId ltrId in layerIds)
{
// Don't try to lock/unlock either the current layer or layer 0
// (depending on whether lockZero == true for the latter)
if (
(!ignoreCurrent || ltrId != db.Clayer) &&
(lockZero || ltrId != db.LayerZero)
)
{
// Open the layer for write and lock/unlock it
var ltr = (LayerTableRecord)tr.GetObject(ltrId, OpenMode.ForWrite);
ltr.IsLocked = dolock;
ltr.IsOff = ltr.IsOff; // This is needed to force a graphics update
}
}
tr.Commit();
}
// These two calls will result in the layer's geometry fading/unfading
// appropriately
ed.ApplyCurDwgLayerTableChanges();
ed.Regen();
}
public static ObjectIdCollection SelectLayers(this Document doc)
{
var db = doc.Database;
var ed = doc.Editor;
// A list of the layers' names & IDs contained
// in the current database, sorted by layer name
var ld = new SortedList<string, ObjectId>();
// A list of the selected layers' IDs
var lids = new ObjectIdCollection();
// Start by populating the list of names/IDs
// from the LayerTable
using (var tr = db.TransactionManager.StartOpenCloseTransaction())
{
var lt = (LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead);
foreach (ObjectId lid in lt)
{
var ltr = (LayerTableRecord)tr.GetObject(lid, OpenMode.ForRead);
ld.Add(ltr.Name, lid);
}
}
// Display a numbered list of the available layers
ed.WriteMessage("\nLayers available:");
ed.SelectNamedLayers(ld, lids);
return lids;
}
private static void SelectNamedLayers(
this Editor ed,
SortedList<string, ObjectId> ld,
ObjectIdCollection lids
)
{
int i = 1;
foreach (KeyValuePair<string, ObjectId> kv in ld)
{
ed.WriteMessage("\n{0} - {1}", i++, kv.Key);
}
// We will ask the user to select from the list
var pio = new PromptIntegerOptions("\nEnter number of layer to add: ");
pio.LowerLimit = 1;
pio.UpperLimit = ld.Count;
pio.AllowNone = true;
// And will do so in a loop, waiting for Escape or Enter to terminate
PromptIntegerResult pir;
do
{
// Select one from the list
pir = ed.GetInteger(pio);
if (pir.Status == PromptStatus.OK)
{
// Get the layer's name
string ln = ld.Keys[pir.Value - 1];
// And then its ID
ObjectId lid;
ld.TryGetValue(ln, out lid);
// Add the layer'd ID to the list, if it's not already on it
if (lids.Contains(lid))
{
ed.WriteMessage("\nLayer \"{0}\" has already been selected.", ln);
}
else
{
lids.Add(lid);
ed.WriteMessage("\nAdded \"{0}\" to selected layers.", ln);
}
}
} while (pir.Status == PromptStatus.OK);
}
}
public class Commands
{
[CommandMethod("LL")]
public void LockLayers()
{
var doc = Application.DocumentManager.MdiActiveDocument;
if (doc == null) return;
doc.LockOrUnlockLayers(true);
}
[CommandMethod("UL")]
public void UnlockLayers()
{
var doc = Application.DocumentManager.MdiActiveDocument;
if (doc == null) return;
doc.LockOrUnlockLayers(false);
}
[CommandMethod("UFC")]
public void UnlockForCommands()
{
var doc = Application.DocumentManager.MdiActiveDocument;
if (doc == null) return;
var ed = doc.Editor;
// Store the selected commands in a list
var cmds = new List<string>();
PromptResult pr;
bool cmdEntered;
do
{
pr = ed.GetString("\nEnter name of command for which to unlock layers");
cmdEntered =
pr.Status == PromptStatus.OK && !String.IsNullOrEmpty(pr.StringResult);
if (cmdEntered)
cmds.Add(pr.StringResult.ToUpper());
}
while (cmdEntered);
if (pr.Status == PromptStatus.Cancel)
return;
var ids = doc.SelectLayers();
var uc = doc.UserData[UnlockCommands.RecordName] as UnlockCommands;
foreach (string cmd in cmds)
{
if (uc.CommandMap.ContainsKey(cmd))
{
var existing = uc.CommandMap[cmd];
foreach (ObjectId id in ids)
{
if (!existing.Contains(id))
{
existing.Add(id);
}
}
}
else
{
uc.CommandMap.Add(cmd, ids);
}
}
if (cmds.Count > 0 && ids.Count > 0)
{
ed.WriteMessage(
"\n{0} command{1} will now unlock {2} layers.",
cmds.Count, cmds.Count == 1 ? "" : "s", ids.Count
);
}
}
}
}
Here's the UFC command in action:
You can see that when we MOVE the layers unlock – relocking again, afterwards – but the same doesn't happen for COPY.