Another really interesting, developer-oriented feature in AutoCAD 2013 is something we’ve been calling “Dynamic .NET”. I don’t know whether that’s official branding, or not – I suspect not – but it’s the moniker we’ve been using to describe this capability internally and to ADN members.
The capability is based on an addition to .NET in version 4.0: to complement (or perhaps just as part of) the integration of the Dynamic Language Runtime into the core .NET Framework, various interfaces – including IDynamicMetaObjectProvider – were provided to developers to let their objects participate in “dynamic” operations.
Providers of APIs therefore now have the option to make certain classes dynamic – for instance, having them implement the IDynamicMetaObjectProvider interface – which means they can effectively be scripted, without binding properties and methods at compile-time. This implements a concept known as duck typing, and is at the core of many scripting language implementations. You may also hear it referred to as late binding: take a look at this excellent post by Eric Lippert if you’d like to get a better understanding of what that term means.
So what have we actually done with AutoCAD 2013? We have made Autodesk.AutoCAD.DatabaseServices.ObjectId support dynamic operations: you can declare an ObjectId using the dynamic keyword in C#, and you can then choose to access any of the properties or methods exposed by the object possessing that ID directly from the ObjectId itself. The ObjectId then takes care of the open & close operations implicitly, so there’s no need to mess around with transactions, etc.
If you’re interested in more background to this, I suggest taking a look at Albert Szilvasy’s class from AU 2009, where he gave us a hint of what was to come (although it was far from being a certainty, at that time – we have since found it to be a popular option via our annual API wishlist surveys).
OK, so what does this mean, in practice? Here’s some statically-typed C# code that iterates through the layer table and prefixes all layers (other than layer “0”) with a string:
public static void ChangeLayerNames()
{
Database db = HostApplicationServices.WorkingDatabase;
using (Transaction tr = db.TransactionManager.StartTransaction())
{
LayerTable lt =
(LayerTable)tr.GetObject(
db.LayerTableId, OpenMode.ForRead
);
foreach (ObjectId ltrId in lt)
{
LayerTableRecord ltr =
(LayerTableRecord)tr.GetObject(ltrId, OpenMode.ForRead);
if (ltr.Name != "0")
{
ltr.UpgradeOpen();
ltr.Name = "First Floor " + ltr.Name;
}
}
tr.Commit();
}
}
Here’s how the code could look if using dynamic types. I’ve done my best not to exaggerate the differences in numbers of lines by using different conventions in the two implementations, but I did have to add more line breaks in the above code to have it fit the width of this blog.
public static void ChangeLayerNamesDynamically()
{
dynamic layers =
HostApplicationServices.WorkingDatabase.LayerTableId;
foreach (dynamic l in layers)
if (l.Name != "0")
l.Name = "First Floor " + l.Name;
}
Something that needs to be stressed: as the properties and methods are effectively being determined and then accessed/called at run-time, there is no IntelliSense available when coding dynamically (something Python and Ruby developers are used to, but we statically-typed .NET developers are probably less so). It would not be impossible to implement, but it would be very difficult: aside from types being inferred at the time code is being written (I’ll use the term develop-time, although I haven’t seen it used before), there would need to be some work done to effectively populate the list of properties & methods, which would typically mean opening the object and reflecting over its protocol… not at all straightforward to implement, as far as I can tell, especially when you don’t actually have AutoCAD running to help with accessing the objects.
A lack of IntelliSense does mean this style of coding it likely to be less accessible to novice coders, unless a more integrated REPL environment becomes available (as this is something that tends to balance out the lack of other develop-time tooling when scripting – just as people have found with the ability to execute fragments of AutoLISP at AutoCAD’s command-line, over the years).
I can see a couple of ways a REPL might end up being implemented. One would be to host the DLR and (say) the IronPython scripting engine (just as Albert showed during his above-mentioned AU class, as I did at my own AU 2009 class, and we’ve seen previously on this blog). Another would be to wait for the fruits of the future .NET Framework feature codenamed Roslyn, which enables Compiler as a Service for .NET code, as this seems it would be a likely way to execute C# and VB.NET in a more dynamic way.
But that’s all for the future, and is very much my own speculation, at this stage.
One question that has come up in relation to “Dynamic .NET” in AutoCAD is around performance: as we’re effectively opening and closing an object every time we execute a method (and we may actually open and close an object twice – once for read, once for write – when we read one of its properties, modify it and store it back), isn’t there a performance impact? I admit I haven’t performed extensive benchmarking of this, and there is likely to be some overhead if dealing with lots of properties and methods. But in terms of getting something working quickly – which you can then tune for performance later, as needed – this is likely to be a valuable tool for developers, irrespective of the performance impact (which I’d hope to be modest).
And it really comes into its own when used in combination with LINQ. Here’s some code that Stephen Preston put together to test out some of the possibilities with LINQ (I’ve included the CLN and CLN2 commands – shown above – for completeness).
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.EditorInput;
using System.Collections.Generic;
using System.Linq;
namespace DynamicTyping
{
public class Commands
{
[CommandMethod("CLN")]
public static void ChangeLayerNames()
{
Database db = HostApplicationServices.WorkingDatabase;
using (
Transaction tr = db.TransactionManager.StartTransaction()
)
{
LayerTable lt =
(LayerTable)tr.GetObject(
db.LayerTableId, OpenMode.ForRead
);
foreach (ObjectId ltrId in lt)
{
LayerTableRecord ltr =
(LayerTableRecord)tr.GetObject(ltrId, OpenMode.ForRead);
if (ltr.Name != "0")
{
ltr.UpgradeOpen();
ltr.Name = "First Floor " + ltr.Name;
}
}
tr.Commit();
}
}
[CommandMethod("CLN2")]
public static void ChangeLayerNamesDynamically()
{
dynamic layers =
HostApplicationServices.WorkingDatabase.LayerTableId;
foreach (dynamic l in layers)
if (l.Name != "0")
l.Name = "First Floor " + l.Name;
}
// Adds a custom dictionary in the extension dictionary of
// selected objects. Uses dynamic capabilities, but not LINQ.
[CommandMethod("AddMyDict")]
public void AddMyDict()
{
Document doc = Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
PromptSelectionResult res = ed.GetSelection();
if (res.Status != PromptStatus.OK)
return;
foreach (dynamic ent in res.Value.GetObjectIds())
{
dynamic dictId = ent.ExtensionDictionary;
if (dictId == ObjectId.Null)
{
ent.CreateExtensionDictionary();
dictId = ent.ExtensionDictionary();
}
if (!dictId.Contains("MyDict"))
dictId.SetAt("MyDict", new DBDictionary());
}
}
// Adds a custom dictionary in the extension dictionary of
// selected objects, but this time using dynamic capabilities
// with LINQ. Conceptually simpler, but with some performance
// overhead, as we're using two queries: one to get entities
// without extension dictionaries (and then add them) and the
// other to get entities with extension dictionaries.
[CommandMethod("AddMyDict2")]
public void AddMyDict2()
{
PromptSelectionResult res =
Application.DocumentManager.MdiActiveDocument.Editor.
GetSelection();
if (res.Status != PromptStatus.OK)
return;
// Query for ents in selset without ExtensionDictionaries
var noExtDicts =
from ent in res.Value.GetObjectIds().Cast<dynamic>()
where ent.ExtensionDictionary == ObjectId.Null
select ent;
// Add extension dictionaries
foreach (dynamic ent in noExtDicts)
ent.CreateExtensionDictionary();
// Now we've added the ext dicts, we add our dict to each
var noMyDicts =
from ent in res.Value.GetObjectIds().Cast<dynamic>()
where !ent.ExtensionDictionary.Contains("MyDict")
select ent.ExtensionDictionary;
foreach (dynamic dict in noMyDicts)
dict.SetAt("MyDict", new DBDictionary());
}
// Access various bits of information using LINQ
[CommandMethod("IUL")]
public void InfoUsingLINQ()
{
Document doc = Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
dynamic bt = db.BlockTableId;
// Dynamic .NET loop iteration
ed.WriteMessage("\n*** BlockTableRecords in this DWG ***");
foreach (dynamic btr in bt)
ed.WriteMessage("\n" + btr.Name);
// LINQ query - returns startpoints of all lines
ed.WriteMessage(
"\n\n*** StartPoints of Lines in ModelSpace ***"
);
dynamic ms = SymbolUtilityServices.GetBlockModelSpaceId(db);
var lineStartPoints =
from ent in (IEnumerable<dynamic>)ms
where ent.IsKindOf(typeof(Line))
select ent.StartPoint;
foreach (Point3d start in lineStartPoints)
ed.WriteMessage("\n" + start.ToString());
// LINQ query - all entities on layer '0'
ed.WriteMessage("\n\n*** Entities on Layer 0 ***");
var entsOnLayer0 =
from ent in (IEnumerable<dynamic>)ms
where ent.Layer == "0"
select ent;
foreach (dynamic e in entsOnLayer0)
ed.WriteMessage(
"\nHandle=" + e.Handle.ToString() + ", ObjectId=" +
((ObjectId)e).ToString() + ", Class=" + e.ToString()
);
ed.WriteMessage("\n\n");
// Using LINQ with selection sets
PromptSelectionResult res = ed.GetSelection();
if (res.Status != PromptStatus.OK)
return;
// Select all entities in selection set that have an object
// called "MyDict" in their extension dictionary
var extDicts =
from ent in res.Value.GetObjectIds().Cast<dynamic>()
where ent.ExtensionDictionary != ObjectId.Null &&
ent.ExtensionDictionary.Contains("MyDict")
select ent.ExtensionDictionary.Item("MyDict");
// Erase our dictionary
foreach (dynamic myDict in extDicts)
myDict.Erase();
}
}
}
You’ll see that certain operations become very straightforward when combining Dynamic .NET and LINQ. This could well be the “sweet spot” of the dynamic capability – it certainly makes LINQ a more natural choice when accessing various types of data in AutoCAD drawings.
That’s it for today’s post. Next time we’ll summarize the remaining API enhancements coming in AutoCAD 2013, before looking at the migration steps needed for .NET applications.