This is the sixth in a series of posts on LINQ an the AutoCAD .NET API. Here’s a complete list of posts in this series.
This is part 6 of our series on LINQ and the AutoCAD .NET API. Currently the GeneralHelper class only support read operations. If we perform a write operation on one of the objects we pulled out of the database, we get an exception. In this blog post we want to correct this behaviour.
On the level of a single database object, the DBObject class provides two methods that let us change the open mode of a DBObject: UpgradeOpen() makes a DBObject writable, DowngradeOpen() makes it read only. Since the for-read-mode is currently our default mode, we first look at how to find a procedure to open a collection of DBObjects for for-write.
Collections
When we use the GeneralHelper class, which collections are we dealing with? Well, the access properties of the GeneralHelper class are of type IEnumerable, where T is derived from DBObject. Since every .NET collection implements IEnumerable, we could define an extension method for IEnumerable and restrict T to DBObject. In other words: whenever a .NET container class contains items that are derived from DBOBject, our extension method to make items for-write would become available. Let’s look at the implementation:
public static class IEnumerableExtensions { public static IEnumerable<T> ForWrite<T>(this IEnumerable<T> source) where T : DBObject { foreach (var item in source) { if (!item.IsWriteEnabled) { item.UpgradeOpen(); } yield return item; } } public static void ForEach<T>(this IEnumerable<T> source, Action<T> action) { foreach (var item in source) { action(item); } } }
The implementation of ForWrite is straightforward: we iterate the source items, which are DBObjects due to the type constraint, and upgrade each item, in case it is not yet write-enabled. The ForEach method is just for convenience, to easily iterate elements of an enumerable of DBObjects.
Now as an example, to delete all lines and circles from the model space, we now can do the following:
[CommandMethod("DeleteLinesAndCircles")] public static void DeleteLinesAndCircles() { using (var helper = new GeneralHelper()) { helper.ModelSpace .OfType<Line>() .ForWrite() .ForEach(l => l.Erase()); var circles = new List<Circle>(); circles.AddRange(helper.ModelSpace .OfType<Circle>()); circles.ForWrite() .ForEach(c => c.Erase()); } }
As we can see, the ForWrite method is not tied to any of our GeneralHelper methods or properties, but also works perfectly fine with a list of Circles (line 14).
For the sake of completeness, we can add a ForRead extension method as well:
public static IEnumerable<T> ForRead<T>(this IEnumerable<T> source) where T : DBObject { foreach (var item in source) { if (item.IsWriteEnabled) { item.DowngradeOpen(); } yield return item; } }
This method simply downgrades a DBObject to for-read, if it is opened for-write.
Commit()
If we run the example in Listing 2, we don’t get any error, because the objects are in the correct state: we want to make changes to the objects and their state is changed to for-write via the call to ForWrite(). But in the transaction based world of AutoCAD objects, opening objects for-write is only the first step. To make any changes persistend, we finally have to commit the transaction.
So for starters, it would be OK to commit the transaction at the latest possible moment, that is, just right before the transaction is disposed. So we simply add the commit to the Dispose method of the GeneralHelper class:
public void Dispose() { if (tr != null && !tr.IsDisposed) { tr.Commit() tr.Dispose(); } }
With this change, the example in Listing 2 works perfectly fine and the changes are persisted in the database.
What’s next
By now we’ve come pretty far. The GeneralHelper class now supports read and write operations in a convenient way. In the next post we will have a look at how to improve the creation of objects and we will do some refactoring. Stay tuned.
Hi Mr Wolfgang,
If you have a ForWrite method – does that mean that there is another iteration? What about passing OpenMode as a variable instead – and then handling it accordingly there?
rgds
Ben
Hi Ben,
yes, it would be fine to add an argument. But for the library this would mean that the properties (like the ModelSpace property) had to be methods. Then one could pass OpenMode as an argument (like ModelSpace(OpenMode.ForWrite) ).
And correct, in the case of ForWrite() there is more code to be executed as the objects are openend ForRead from the database and explicitly changed to ForWrite via UpgradeOpen(). So passing an OpenMode argument would be slightly faster. But I decided for properties instead of methods, I think this makes the code more readable. Desinging an API means making compromises…
-Wolfgang