To accompany the last post – which raised some questions around when and where to call Dispose() on objects created or accessed via AutoCAD’s .NET API – today we’re going to look at a few concrete examples.
Thanks to Danny P for not only requesting some examples but also presenting some concrete areas he wasn’t fully clear on. Let’s start by looking at those (and feel free to compare the responses I’ve put below with the ones I made in direct response to Danny’s original comment):
Within a transaction where something is added to the database, some new objects (Xrecords, dictionaries) can be relatively simple and might only need a Using block. Conversely, complex objects (dimension styles, block definitions with attributes) have many properties and resulting code with potential points of failure. How would you deal with that in a Try...Catch block + Using block to handle exceptions at the object level and the transaction level?
The short answer is that anything managed by a transaction – whether newly created and added to the transaction or opened by it – will be disposed of automatically by the transaction. So in general you shouldn’t even need a using block around such entities.
For more complex scenarios the same is also true, although if you’re creating a database-resident definition object, for instance, then you’d need to make sure that it also gets added to the transaction.
In the case where a new object is “owned” by another new, more complex object (such as a FontDescriptor being owned by a TextStyleTableRecord via its Font property), then you can generally assume that just by setting the property on the owner you’ve absolved yourself of needing to Dispose() of it yourself. The example I’ve used isn’t a great one, admittedly – FontDescriptors don’t actually require disposal – but hopefully you get the point I’m trying to make.
A second thing that I found was casting Entities to specific object types, for example iterating through block definitions looking for a line, and casting that Entity to a Line object. Do I need to dispose of the Line because I didn't use line = transaction.GetObject(...), but rather line = TryCast(entity, Line)? In other words, is the new object a "transaction-managed" object, or do I need to dispose of it, or is it not a new object to the database and I don't need to worry about it?
This is an interesting point. In this situation, the object reference in the Line variable is not actually created by the cast (your TryCast() call in VB.NET). It was created by the call to Transaction.GetObject(): all the cast does is attempt to coerce the reference into a variable of another type (after it has first checked that the reference object also support the Line protocol). The object reference between the Entity and Line variables are one and the same – with the same, underlying, unmanaged pointer – it’s just they’re conveniently held in variables of different types that allow compile time features such as Intellisense and static type checking (even if the TryCast() could conceivably fail at runtime, if the object didn’t happen to be a Line). So as long as the original object reference is managed by a transaction – and in this case you’ve received it from the transaction – then all is well.
Now let’s take a look at a couple of examples of where you might add calls to Dispose() in new code. I started by looking for previous posts with “theoretically” problematic code, and found this one.
Here’s the code in question:
// Create the mirror line and the transformation matrix
Line3d ml = new Line3d(pt1, pt2);
MirrorEntity(doc.Database, per.ObjectId, ml, false);
Basically we’re not calling Dispose() on the temporary Line3d object after the MirrorEntity() method has completed. We could simply adjust the code as follows to have the Line3d get disposed at the end of the using block:
// Create the mirror line and the transformation matrix
using (Line3d ml = new Line3d(pt1, pt2))
{
MirrorEntity(doc.Database, per.ObjectId, ml, false);
}
Again, this is not a change that you absolutely need to go back and perform in your code – I have every expectation that this code will continue working properly in future versions of AutoCAD without Line3d being disposed of – but we’re really talking about avoiding problems that are theoretically possible.
Now let’s take a look at code in another post that uses the potentially problematic Brep API identified in the last post. (Both the examples I’ve linked to happen to have been provided by other people – that’s honestly not a deliberate choice, they just happen to have been the ones I found that best demonstrate the issue. :-)
When I ran this code again from the debugger, stepped through it and then closed AutoCAD, I actually did receive an exception that seems related to this issue:
It’s possible this isn’t due to this problem – which is by its nature intermittent and tricky to catch – but the fact it’s in the destructor of an AcGe object seems a giant red flag.
Here’s a revamped version of the C# code. I ended up going a bit crazy on introducing the use of var instead of specific types (although you shouldn’t worry – this code is still perfectly typesafe as all types are inferred at design time), which hopefully doesn’t distract you from the more important changes (which were to introduce a manual Dispose() and a number of using blocks).
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.BoundaryRepresentation;
using AcBr = Autodesk.AutoCAD.BoundaryRepresentation;
using Autodesk.AutoCAD.Colors;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using AcGe = Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using System.Collections.Generic;
using System;
// Not mandatory, but improves loading performance
[assembly: CommandClass(typeof(HoleFeature.MyCommands))]
namespace HoleFeature
{
public class MyCommands
{
[CommandMethod("GETHOLES")]
public void GetHoles()
{
var doc = Application.DocumentManager.MdiActiveDocument;
var db = doc.Database;
var ed = doc.Editor;
var peo = new PromptEntityOptions("\nSelect a 3D solid: ");
peo.SetRejectMessage("\nMust be a 3D solid.");
peo.AddAllowedClass(typeof(Solid3d), true);
var per = ed.GetEntity(peo);
if (per.Status != PromptStatus.OK)
return;
var tr = db.TransactionManager.StartTransaction();
using (tr)
{
var solid =
tr.GetObject(per.ObjectId, OpenMode.ForWrite) as Solid3d;
var ids = new ObjectId[] { solid.ObjectId };
var path =
new FullSubentityPath(
ids,
new SubentityId(SubentityType.Null, IntPtr.Zero)
);
// For storing SubentityIds of cylinderical faces
var subentIds = new List<SubentityId>();
using (var brep = new Brep(path))
{
foreach (var face in brep.Faces)
{
using (var surf = face.Surface)
{
var ebSurf = surf as ExternalBoundedSurface;
// We are only looking only cylinders
if (ebSurf != null && ebSurf.IsCylinder)
{
var cyl = ebSurf.BaseSurface as Cylinder;
// And fully closed cylinders
if (cyl != null && cyl.IsClosed())
{
// Get normal and point on surface
var normal = new Vector3d();
var pt = new Point3d();
GetNormalAndPoint(surf, ref normal, ref pt);
if (IsHole(face, normal, pt, cyl))
{
subentIds.Add(face.SubentityPath.SubentId);
}
}
}
}
face.Dispose();
}
}
// Assign red color to hole features
if (subentIds.Count > 0)
{
short colorIdx = 1;
AssignColor(solid, subentIds, colorIdx);
}
tr.Commit();
}
}
// Get normal and point at mid U and V parameters
void GetNormalAndPoint(
AcGe.Surface surf, ref Vector3d normal, ref Point3d pt
)
{
var box = surf.GetEnvelope();
double p1 = box[0].LowerBound + box[0].Length / 2.0;
double p2 = box[1].LowerBound + box[1].Length / 2.0;
var ptParams = new Point2d(p1, p2);
var pos = new PointOnSurface(surf, ptParams);
normal = pos.GetNormal(ptParams);
pt = pos.GetPoint(ptParams);
}
// A cylinder is a hole if the normal points inwards
// and the normal after extending by radius intersects
// with axis of symmetry, the axis of symmetry is also
// extended by height of cylinder
Boolean IsHole(
AcBr.Face face, Vector3d normal, Point3d pt, Cylinder cyl
)
{
if (!face.IsOrientToSurface)
{
// Correct the normal and save back
normal = normal.Negate();
}
// Calculate another point on normal by extending the
// normal by radius of cylinder
var opt =
new Point3d(
pt.X + normal.X * cyl.Radius,
pt.Y + normal.Y * cyl.Radius,
pt.Z + normal.Z * cyl.Radius
);
// Get the cylinder's axis
var v1 = cyl.AxisOfSymmetry;
double dist = cyl.Height.Length;
// Calculate another point on axis by extending v1 by dist
var pt2 =
new Point3d(
cyl.Origin.X + v1.X * dist,
cyl.Origin.Y + v1.Y * dist,
cyl.Origin.Z + v1.Z * dist
);
// Create line segment representing the cylinder's normal
// Create line segment representing the cylinder's axis
Point3d[] intpt = null;
using (var ls1 = new LineSegment3d(pt, opt))
using (var ls2 = new LineSegment3d(cyl.Origin, pt2))
{
// Get intersection of normal and cylinder axis
intpt = ls1.IntersectWith(ls2);
}
return (intpt != null);
}
// Assign color to cylinderical surfaces which are holes
void AssignColor(
Solid3d solid, List<SubentityId> subentIds, short idx
)
{
foreach (SubentityId subentId in subentIds)
{
var col = Color.FromColorIndex(ColorMethod.ByColor, idx);
solid.SetSubentityColor(subentId, col);
}
}
}
}
One thing that’s a little peculiar with the Brep API is that you’re actually forcing a Dispose() on objects that are properties of the Brep object itself (objects that are instantiated when you access the property). That doesn’t always seem right – you would expect the using block around the Brep object to force a Dispose() on all associated objects created/exposed via its properties – but this a quirk of the Brep API that needs special attention, and a big reason the GC-driven finalisation of unmanaged objects seems to rear its ugly head more often when it’s used.
The good news is that the results of over-aggressive disposing – you should see some kind of exception at object disposal – are going to be obvious much more quickly than those of inadequate disposing, which will probably take some time to identity. So it’s generally better to dispose more often than perhaps strictly needed when using the Brep API, but to pay special attention to exceptions (if you blindly catch and ignore every exception that gets thrown, you may run the risk of missing such issues).
Hopefully looking at a few concrete examples has helped improve the understanding of this tricky area. Be sure to post a comment if you have follow-up questions regarding this topic.