There are two reasons why I decided to start with the implementation of a document manager for ‘Colibra’. The first one is to overcome the limitations the current Civil 3D API has regarding document management. The second one was out of necessity, since I need to be able to manage different documents as I write unit tests for the library.
The Civil 3D API only provides support to access the current active document. Most of the time, this is all you need. The commands and extensions you will write for Civil 3D should be based on one document, and most of the time, this will be the current document. Nevertheless, sometimes you will need to open other documents or interact with more than one document at a time. This can easily be solved by implementing your own document manager, which I will demonstrate in this post.
I started with a simple implementation of the ‘DocumentManager’ class and more functionality will be added as needed. For now, I just provide two facilities; access to the current document, and the ability to open a document from a ‘DWG’ file.
As soon as I started to implement ‘DocumentManager’, it became clear that I needed also a ‘Document’ class. Civil 3D provides a ‘CivilDocument’ class, which is the entry point to all the Civil 3D objects in a drawing, and AutoCAD provides a different ‘Document’ class for AutoCAD based objects. Handling different types of document objects can become a nightmare very quickly, and a good solution is to wrap all the access in a custom ‘Document’ class and provide the necessary interface to access all kinds of objects.
One of the issues we are confronted with when providing a wrapper for the different document objects is to keep them in sync. This is due to the fact that AutoCAD uses the ‘Document’ object as the owner of the context in which our commands are running, which in turn causes the ‘DocumentManager.MdiActiveDocument’ property to always return the instance of the document in which our command is running. This is true for every command that runs in a document context. We can circumvent this issue by running the command in a session context, but there are other limitations on doing this, and we will end up with different application behavior depending on the context we are running.
A better solution is to have a document manager, the ‘DocumentManager’ class in our example, which provides support for the different available documents. Our ‘DocumentManager’ class provides access to custom ‘Document’ objects, which abstract the complexity of accessing the different AutoCAD and Civil 3D document instances and provide the interface required by our application. In any case, there are a couple of assumptions/decisions we have to make when implementing ‘DocumentManager’ and ‘Document’.
The first assumption is that on first request, and if no other document has been opened, the active document is the document providing the execution context, that is the document from which our command has been launched. If we open a document using our ‘DocumentManager’ class, the new document will become our active document in the context of our application (i.e. in the context of our ‘DocumentManager’ class). This is required because we will have to activate the new document in order for the Civil 3D API to gain access to it.
The second assumption is that we will only manage documents using ‘DocumentManager’ and that access to objects will only happen through our implementation of the ‘Document’ class. To successfully implement ‘DocumentManager’ and ‘Document, we need to keep some state related to our application, which will not necessarily match the state of the context AutoCAD is keeping in its implementation. If we don’t follow this norm, we will run into issues very easy,and we may end up accessing the wrong document object in our code.
In general, I discourage keeping state in wrapper classes because it is very hard to keep them in sync with the model when things are modified outside the wrapper interface. However, I provide wrappers to insure all access to objects are done through them, which gives me a little bit more of control. You may find yourself in a similar situation, and my recommendation is to use state aware wrappers only as the last resource. Some people like to cache certain state for performance reasons. My take is to make sure my code is correct first, and then profile and optimize if performance is an issue.
Document Class Implementation
Let’s take a quick look at the implementation of our ‘Document’ class, which will wrap access to the different document objects.
public class Document { internal Document(acadappsvcs.Document acadDoc, CivilDocument civilDoc) { m_ThisAcadDocument = acadDoc; m_ThisCivilDocument = civilDoc; } public string Name { get { return m_ThisAcadDocument.Name; } } public void Activate() { DocumentManager._activateDocument(this); } internal acadappsvcs.Document _acaddoc { get { return m_ThisAcadDocument; } } private acadappsvcs.Document m_ThisAcadDocument; private CivilDocument m_ThisCivilDocument; }
The implementation defines an internal constructor that takes the AutoCAD document instance, as well as the Civil 3D document instance. The constructor is declared internal because we want to discourage clients from creating ‘Document’ objects using the ‘new’ operator. We set this restriction, so every ‘Document’ instance has to be accessed through the ‘DocumentManager’ class, which is going to keep the state.
The implementation exposes a property ‘Name’ that returns the name of the document using the AutoCAD document instance, and a method ‘Activate()’ that allows activating the document. In the future, we will extend the ‘Document’ class to provide access to objects as needed.
DocumentManager Class Implementation
The current implementation of ‘DocumentManager’ defines a set of static methods and properties that expose a basic interface to access the current active document and to open a new document from a ‘DWG’ file. We will extend the interface to add more capabilities in the future, but another thing that will change is the static nature of the class. There are many reasons why ‘DocumentManager’ should not be static and expose a static interface, one of them is that we may run into issues when using the class from different commands that run in sequence, or that change document objects, but for illustration purposes in this post, the current implementation should be good enough.
public class DocumentManager { public static Document ActiveDocument { get { if (m_ActiveDocument == null) { // We were never called, so lets initialize the class to the // current active document in AutoCAD. // createNewAndActivateFromAutoCADDocument(acadappsvcs.Application.DocumentManager.MdiActiveDocument); } return m_ActiveDocument; } } public static Document OpenDocument(string fileName) { acadappsvcs.Document acadDoc = acadappsvcs.Application.DocumentManager.Open(fileName); createNewAndActivateFromAutoCADDocument(acadDoc); return m_ActiveDocument; } internal static void _activateDocument(Document doc) { acadappsvcs.Application.DocumentManager.MdiActiveDocument = doc._acaddoc; m_ActiveDocument = doc; } private static void createNewAndActivateFromAutoCADDocument(acadappsvcs.Document acadDoc) { CivilDocument civilDoc = getCivilDocumentAndActivate(acadDoc); m_ActiveDocument = new Document(acadDoc, civilDoc); } private static CivilDocument getCivilDocumentAndActivate(acadappsvcs.Document acadDoc) { acadappsvcs.Application.DocumentManager.MdiActiveDocument = acadDoc; return CivilApplication.ActiveDocument; } private static Document m_ActiveDocument = null; }
There are a couple of things I want to explain about the implementation. First when the ‘ActiveDocument’ property is invoked, we check if there is already an active document. If it isn’t, we assume the current AutoCAD document (the one from where we invoked the command) is the active document, and we insure ‘DocumentManager’ keeps it in its state. Otherwise, we return the document our ‘DocumentManager’ believes is active.
The second thing I want to clarify is that when we open a document, we need to activate it, so the Civil 3D API can access it and keep a reference to its instance. Once this happens, there will be a mismatch between the AutoCAD API ‘DocumentManager.MdiActiveDocument’ property and the ‘CivilApplication.ActiveDocument’ property. In our implementation, we keep the state of the later, but we need to insure all ‘Document’ access is done through our custom ‘DocumentManager’ wrapper class.
About the Source
You can download Build 2 of Colibra
to review the complete source code and step through the implementation. Make sure you familiarize your self with the code because we will use it extensively in future posts. Also, I am happy to announce that per readers’ request, I decided to provide a VB.NET version of the Colibra library and all the unit tests, which you will find also in Build 2. Many users are transitioning from VBA to VB.NET, and I am sure they will find this helpful.Finally, make sure you use the comments area to provide me with any feedback or comments you might have. I’d like to hear from you and your opinions about the implementation.
Hi Isaac,
Wanted to say thank you for starting this blog. It looks to be a great resource for us part-time C3D programmers to be able to learn far more than we'd be able to on our own.
One comment: your blog format is cutting off code that extends past the right margin. I know you are providing the source code, but it makes it difficult to follow along reading the text.
Looking forward to the next installment!
Jeff
Posted by: Jeff Mishler | 03/29/2011 at 07:48 AM
Hi Jeff,
Thanks for your comment. I am not happy with the way source code gets posted. I got to look into different alternatives, but so far, I have not found the best way to embed the source. The problem is the post column being too narrow, which makes it very difficult to embed the code in a font size that it's still readable. But I will keep playing with it to see if I find a good solution.
Posted by: Isaac Rodriguez | 03/29/2011 at 10:27 AM