While programming an AutoCAD plugin, there are many occasions we use Document.SendStringToExecute() method to existing command, which can be built-in commands or custom commands (including lisp-defined commands).
Using SendStringToExecute() could save a lot of programming effort for particular AutoCAD operations when we know there are existing commands/lisp routines that does exactly the work, especially when the existing commands/lisp routines do some work that we programmers cannot easily do with our own code, and in many case, why reinventing the wheel anyway?
However, due to its "async" nature, it cannot be used in middle of a chunk of our code and expect the code following it can operate on the result produced from the SendStringToExecute() call. For example, if we need to call SendStringToExecute() repeatedly as loop in a custom operation of our code, following code simply does not work:
private void DoRepeatedWork(IEnumerable<ObjectId> entIds)
{
var dwg = Application.DocumentManager.MdiActiveDocument;
foreach (var entId in entIds)
{
// set PickFirst selections
dwg.SendStringToExecute("SomeCommand\n", false, false, false);
// Do something with the result of SendStringToExecute()
}
}
The key to be able to repeatedly call SendStringToExecute() is to know when the command/lisp routine executed by SendStringToExecute() ends. Fortunately, in the .NET API, there are Document.CommandEnded/Cancelled and Document.LispEnded/Cancelled events for us to handle, so that we know when command/lisp routine execution ends, thus, we can repeatedly call SendStringToExecute() in a loop.
Following code example shows how to use SendStringToExecute() to run an command repeatedly against a collection of entities, one at a time. I created a command "ThirdPartyCmd" to mimic a third party command, which we really do not know (or do not need to know) how it does its work inside. But for the sake to show the "third party" command's effect, I let the command only accept one Line entity as PickFirst selection and rotate it, change its color.
#region Handle CommandEnded for looping
private ObjectId[] _selectedLines = null;
private int _currentIndex = 0;
private Document _thisDwg = null;
[CommandMethod("CmdLoop", CommandFlags.Session)]
public void RunLoopingCommand()
{
_thisDwg = CadApp.DocumentManager.MdiActiveDocument;
var ed = _thisDwg.Editor;
// Select target entities when the command loop begins
if (_selectedLines == null)
{
var filter = new SelectionFilter(
new[] { new TypedValue((int)DxfCode.Start, "LINE") });
var res = ed.GetSelection(filter);
if (res.Status == PromptStatus.OK)
{
_selectedLines = res.Value.GetObjectIds();
}
else
{
return;
}
_currentIndex = 0;
}
// if necessary, do something with the
// entity that was processed by SendStringToExecute()
if (_currentIndex>0)
{
//DoSomething(_selectedLines[_currentIndex-1]);
}
_thisDwg.Editor.SetImpliedSelection(new[] { _selectedLines[_currentIndex] });
_thisDwg.CommandEnded += ThirdPartyCmd_Ended;
_thisDwg.SendStringToExecute("ThirdPartyCmd\n", false, false, false);
}
private void ThirdPartyCmd_Ended(object sender, CommandEventArgs e)
{
if (e.GlobalCommandName.ToUpper() != "THIRDPARTYCMD") return;
_thisDwg.CommandEnded -= ThirdPartyCmd_Ended;
_thisDwg.Editor.WriteMessage(
$"\nLine being processed by ThirdPartyCmd: {_selectedLines[_currentIndex]}");
// increment index
_currentIndex++;
_thisDwg.Editor.WriteMessage(
$"\nThirdPartyCmd execution count: {_currentIndex}");
// End the loop after last target entity is done.
if (_currentIndex >= _selectedLines.Length)
{
_selectedLines= null;
return;
}
//Back to original command for another loop
_thisDwg.SendStringToExecute("CmdLoop\n", true, false, true);
}
[CommandMethod("ThirdPartyCmd", CommandFlags.UsePickSet)]
public void RotateEntity()
{
var dwg = CadApp.DocumentManager.MdiActiveDocument;
var ed = dwg.Editor;
var res = ed.SelectImplied();
ed.WriteMessage("\nRunning third party command...,");
if (res.Status == PromptStatus.OK)
{
ed.WriteMessage($"\nProcessing selected entity: {res.Value[0].ObjectId}");
using(var tran=dwg.TransactionManager.StartTransaction())
{
var line = (Line)tran.GetObject(res.Value[0].ObjectId, OpenMode.ForWrite);
line.ColorIndex = new Random().Next(0, 255);
line.TransformBy(Matrix3d.Rotation(
Math.PI / 2.0, Vector3d.ZAxis, line.StartPoint));
tran.Commit();
}
}
ed.WriteMessage("\ndone!");
}
#endregion
The code is quite self-explanatory. See this video clip see the code running result:
This post is actually inspired by this discussion in the .NET forum. The original poster want to execute command "TxtExp" (exploding text, from Express Tools) repeatedly against a selection of text entities, one at a time, and between the "TxtExp" calls, he/she also want to do something with the exploding-generated entities. He/she also referred an article posted by famous Kean Walmsley about using "TxtExp". "TxtExp" is a lisp command from the Express Tool suite. While it is certainly possible to write our own code to do the same text exploding work, but it would be quite complicated. If we could just use it in our code to achieve our goal, we can save a lot of time/effort. So, I gave it a try, use the same approach as above code shows.
The difference is that handling CommandEnded event is changed to handling LispEnded. Also, since we know the "TxtExp" (one can study the lisp routine "txtexp.lsp" in "Express" folder for some inside of this command) command generating a bunch of geometry entities from the selected entities, adding them into database, and then erasing the text, we can somehow collect these "exploded" geometry entities between "TxtExp" calls and do something as needed. Again, following code are quite easy to follow, so no extra explanation is needed:
#region Handle LispEnded for looping
private ObjectId[] _selectedTexts = null;
private List<ObjectId> _explodedEntities = null;
private int _index = 0;
private Document _dwg = null;
[CommandMethod("ExplodeTexts")]
public void ExplodeTextsToGeometryEntities()
{
_dwg = CadApp.DocumentManager.MdiActiveDocument;
var ed = _dwg.Editor;
if (_selectedTexts == null)
{
var filter = new SelectionFilter(new TypedValue[]
{
new TypedValue((int)DxfCode.Start, "TEXT")
});
var res = ed.GetSelection(filter);
if (res.Status != PromptStatus.OK) return;
_selectedTexts = res.Value.GetObjectIds();
_index= 0;
}
if (_index>0)
{
UpdateExplodedEntities();
}
_dwg.LispEnded += Lisp_Ended;
_dwg.Database.ObjectAppended += Entity_Appended;
_explodedEntities = new List<ObjectId>();
ed.SetImpliedSelection(new[] { _selectedTexts[_index] });
_dwg.SendStringToExecute("TXTEXP\n", false, false, false);
}
private void Lisp_Ended(object sender, EventArgs e)
{
ObjectId id = _selectedTexts[_index];
if (id.IsErased)
{
_dwg.LispEnded -= Lisp_Ended;
_dwg.Database.ObjectAppended -= Entity_Appended;
_dwg.Editor.WriteMessage($"\nExploding text count: {_index + 1}");
_index++;
// end looping
if (_index >= _selectedTexts.Length)
{
_selectedTexts = null;
return;
}
// Do next loop
_dwg.SendStringToExecute("ExplodeTexts\n", false, false, false);
}
}
private void Entity_Appended(object sender, ObjectEventArgs e)
{
if (e.DBObject is Entity)
{
_explodedEntities.Add(e.DBObject.ObjectId);
}
}
private void UpdateExplodedEntities()
{
var color = new Random().Next(0, 255);
using (var tran = _dwg.TransactionManager.StartTransaction())
{
foreach (var entId in _explodedEntities)
{
if (!entId.IsErased)
{
var ent = (Entity)tran.GetObject(entId, OpenMode.ForWrite);
ent.ColorIndex = color;
}
}
tran.Commit();
}
}
#endregion
See the video clip below showing the effect of the code running:
Obviously I omitted the code to handle possible CommandCancelled/LispCancelled event, if the command/lisp routine to be executed could be cancelled by design, we may want to handle this event if necessary.