Hi,
IMO the main issue when building .NET LispFunction is how to deal with arguments.
A LispFunction method always have ResultBuffer parameter which contains the arguments of the LISP function.
So, we have to deal with the arguments of a dynamic (un)typed langage as LISP wrapped in TypedValueS in a Resultbuffer and manage this in a staticly/strongly typed langage as C#.
Another question is how to handle wrong arguments (number or type).
Should we don't care and let AutoCAD handle it ? (with the risk AutoCAD crashes)
Should we make the function return nil (or some other value) ? (and let the calling LISP deal with this returned value)
Should we throw a .NET exception ? (to alert the user and stop evaluation)
Here an example which shows different ways to create a LISP function to insert a block in the current space and returns the newly inserted block ename.
The LISP function signature :
(insert blockName inspoint layer colorIndex lineType [attribs])where:
-
blockName is a string
-
inspoint is a LISP point (list of 2 or 3 numbers)
-
scale is LISP number (even an integer or a real)
-
layer is a string
-
colorIndex is an integer
-
lineType is a string
-
attribs (optional) is a list of strings
Each LispFunction will call a private method (InsertBlock) with the upper arguments converted in .NET types
private ObjectId InsertBlock(
string blockName,
Point3d insPoint,
double scale,
string layer,
int colorIndex,
string lineType,
string[] attribs)
{
Document doc = AcAp.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
ObjectId retVal;
using (Transaction tr = db.TransactionManager.StartTransaction())
{
var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
if (!bt.Has(blockName))
throw new ArgumentException
("Block '" + blockName
+ "' not found",
"blockName"); var space = (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
var br
= new BlockReference
(insPoint, bt
[blockName
]); br
.ScaleFactors = new Scale3d
(scale
); br.Layer = layer;
br.ColorIndex = colorIndex;
br.Linetype = lineType;
retVal = space.AppendEntity(br);
tr.AddNewlyCreatedDBObject(br, true);
var btr = (BlockTableRecord)tr.GetObject(bt[blockName], OpenMode.ForRead);
int i = 0;
RXClass attDefClass
= RXClass
.GetClass(typeof(AttributeDefinition
)); foreach (ObjectId id in btr)
{
if (id.ObjectClass == attDefClass)
{
AttributeDefinition attDef = (AttributeDefinition)tr.GetObject(id, OpenMode.ForRead);
AttributeReference attRef
= new AttributeReference
(); attRef.SetAttributeFromBlock(attDef, br.BlockTransform);
if (i < attribs.Length)
attRef.TextString = attribs[i++];
br.AttributeCollection.AppendAttribute(attRef);
tr.AddNewlyCreatedDBObject(attRef, true);
}
}
tr.Commit();
}
return retVal;
}
The simplest, but not safest, way is to let AutoCAD handle the argument exceptions.
AutoCAD will crash if resbuf is null (function called with none argument) or, in case of wrong argument type or number, throw a System exception (InvalidCastException or IndexOutOfRangeException) with a non explicit message at command line.
[LispFunction("insert")]
public ObjectId UnsafeInsert(ResultBuffer resbuf)
{
TypedValue[] args = resbuf.AsArray();
string blockName = (string)args[0].Value;
Point3d insPoint = (Point3d)args[1].Value;
double scale = Convert.ToDouble(args[2].Value);
string layer = (string)args[3].Value;
int colorIndex = Convert.ToInt32(args[4].Value);
string lineType = (string)args[5].Value;
List
<string> attribs
= new List
<string>();
if (args.Length > 6)
{
for (int i = 7; i < args.Length - 1; i++)
{
attribs.Add((string)args[i].Value);
}
}
return InsertBlock(blockName, insPoint, scale, layer, colorIndex, lineType, attribs.ToArray());
}
Another way is to check the arguments contained in the ResultBuffer and make the function return nil in case of a wrong argument.
Here wrong arguments won't throw an error and won't stop the LISP evaluation, so the LISP function which calls (insert ...) should take care of the return value.
[LispFunction("insert")]
public object InsertOrReturnNil(ResultBuffer resbuf)
{
if (resbuf == null)
return null;
TypedValue[] args = resbuf.AsArray();
if (args.Length < 6)
return null;
TypedValue tv = args[0];
if (tv.TypeCode != (int)LispDataType.Text)
return null;
string blockName = (string)tv.Value;
tv = args[1];
Point3d insPoint;
if (tv.TypeCode == (int)LispDataType.Point2d)
{
Point2d pt = (Point2d)tv.Value;
insPoint
= new Point3d
(pt
.X, pt
.Y,
0.0); }
else if (tv.TypeCode == (int)LispDataType.Point3d)
insPoint = (Point3d)tv.Value;
else
return null;
tv = args[2];
double scale;
if (tv.TypeCode == (int)LispDataType.Int16 ||
tv.TypeCode == (int)LispDataType.Int32 ||
tv.TypeCode == (int)LispDataType.Double)
scale = Convert.ToDouble(tv.Value);
else
return null;
tv = args[3];
if (tv.TypeCode != (int)LispDataType.Text)
return null;
string layer = (string)tv.Value;
tv = args[4];
int colorIndex;
if (tv.TypeCode == (int)LispDataType.Int16 ||
tv.TypeCode == (int)LispDataType.Int32)
colorIndex = Convert.ToInt32(tv.Value);
else
return null;
tv = args[5];
if (tv.TypeCode != (int)LispDataType.Text)
return null;
string lineType = (string)tv.Value;
List
<string> attribs
= new List
<string>(); if (args.Length > 6)
{
tv = args[6];
if (tv.TypeCode != (int)LispDataType.ListBegin)
return null;
for (int i = 7; i < args.Length - 1; i++)
{
tv = args[i];
if (tv.TypeCode != (int)LispDataType.Text)
return null;
attribs.Add((string)tv.Value);
}
}
return InsertBlock(blockName, insPoint, scale, layer, colorIndex, lineType, attribs.ToArray());
}
The way I prefer, even I didn't found how to
cleanly throw an exception from LispFunction, is to throw a custom .NET Exception, catch it to alert the user and rethrow to stop evaluation.
This could be done by replacing the 'return null' statements the upper code with a 'throw Exception' one and wrapping the whole in a Try/Catch block to manage the exceptions.
A
thread started by Luis 'LE' Esquivel who purposed to build a .NET LispFunction library, brings me to build some wrappers/helpers to make this process more easy to code. You can find the specific LispException definitions and some extension methods further in this thread (
here and
there).
Using these classes allows to write a more safe/concise/readable code.
[LispFunction("insert")]
public ObjectId SafeInsert(ResultBuffer resbuf)
{
return resbuf.CatchApply(rb =>
{
TypedValue[] args = rb.GetAsArray(6, int.MaxValue);
string blockName = args[0].GetAsString();
Point3d insPoint = args[1].GetAsPoint();
double scale = args[2].GetAsNumber();
string layer = args[3].GetAsString();
int colorIndex = args[4].GetAsShort();
string lineType = args[5].GetAsString();
string[] attribs = null;
if (args.Length > 6)
attribs = args.GetListContent(6).Select(tv => tv.GetAsString()).ToArray();
else
return InsertBlock(blockName, insPoint, scale, layer, colorIndex, lineType, attribs);
});
}