I almost named this post to make it clear it’s about a new API capability in AutoCAD 2015, but then I wouldn’t have had the slightly perverse satisfaction of resurrecting a series of posts after 7.5 years. Here are the first parts in this series, in case you’ve forgotten them (and you’d be forgiven for doing so, after all ;-).
This post is introducing the new PerDocumentClassAttribute capability provided in AutoCAD 2015’s .NET API. We’re going to look at it over two posts: the first showing how it can be used with your own “manager” class to link the data to a Document and the second will show how the Document’s UserData property might also be used to take care of that.
[Thanks to the ADN team for providing the sample I used as a basis for today’s post (it may well have come from the AutoCAD team before then, but I first saw it on the ADN web-site).]
So what does this attribute do? Let’s see what the documentation says:
This custom attribute class is used to mark a type so that an instance of that type will be instantiated for each document open/opened in AutoCAD.
An application may designate as many types as desired with this attribute. When the application is loaded into AutoCAD, a new instance of the type will be instantiated for each document currently open and a reference to the document, and a new instance will be instantiated for each document opened thereafter. When a document is closed, the associated type instance will be disposed if it derives from IDisposable.
The type associated with a PerDocumentClass attribute must provide either a public constructor that takes a Document argument, or a public static Create method that takes a Document argument and returns an instance of the type. If the Create method exists, it will be used, otherwise the constructor will be used. The Document that the type instance is being created for will be passed as the Document argument so that the type instance knows which Document it is associated with.
To paraphrase, this attribute provides a way to specify that a particular type of object needs to be created – and optionally disposed of – for any Documents loaded in the editor (currently loaded Documents have objects created as the application loads, additional objects will be instantiated as Documents get opened or created).
It’s important to note that the mechanism doesn’t specify how the data should be held in memory: as we’ll see in this post and the next, you can choose to manage this via your own manager class or a Document’s UserData map.
To use the mechanism you need to define your class and point to it from an assembly-level attribute. Here’s some C# code showing how a DateTime can be associated with each Document via our custom PerDocData class:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;
using System;
using System.Collections.Generic;
// The all-important attribute telling AutoCAD to instantiate
// the PerDocData class for each open or new document
[assembly: PerDocumentClass(typeof(PerDocSample.PerDocData))]
namespace PerDocSample
{
public class Commands
{
// A simple command to write the contents of our per-document
// data to the command-line
[CommandMethod("PPDD")]
public void PrintPerDocData()
{
var doc = Application.DocumentManager.MdiActiveDocument;
var perDocData = PerDocDataManager.GetData(doc);
doc.Editor.WriteMessage(
perDocData == null ?
"\nNo user data found." :
perDocData.OpenDateTime.ToString()
);
}
}
// We need a manager class that will keep our data mapped to
// a document. This class can optionally care about when
// documents are closed via the Document's BeginDocumentClose
// event
public class PerDocDataManager
{
// Create a mapping between documents and our data objects
static Dictionary<Document, PerDocData> _data =
new Dictionary<Document, PerDocData>();
// We call this static method from our class' constructor,
// so we get added to the map
public static void AddPerDoc(Document doc, PerDocData att)
{
_data.Add(doc, att);
doc.BeginDocumentClose +=
(s, e) =>
{
var d = (Document)s;
if (_data.ContainsKey(d))
_data.Remove(d);
};
}
// A static method to return the data associated with a
// Document
public static PerDocData GetData(Document doc)
{
return _data.ContainsKey(doc) ? _data[doc] : null;
}
}
// Our per-document data class. This will get instanciated for
// each existing or new document: we get the creation
// notification via either the static Create() method or via
// the public constructor that takes a Document argument
public class PerDocData
{
// We will store the time the document was opened or created
private DateTime _openDateTime;
// Provide a public read-only property for that
public DateTime OpenDateTime
{
get { return _openDateTime; }
}
// Public constructor taking a Document
public PerDocData(Document doc)
{
_openDateTime = DateTime.Now;
PerDocDataManager.AddPerDoc(doc, this);
}
// Static Create method: this is the first approach tried
// (to differentiate we're adding an hour to the current
// time, so it's clear this method is being called)
public static PerDocData Create(Document doc)
{
var pdd = new PerDocData(doc);
pdd._openDateTime += TimeSpan.FromHours(1);
return pdd;
}
}
}
One change I made to the sample was to flesh out the two approaches for creating new instances of your tagged class. The first is to provide a Create() factory function that returns a new instance, the second is to provide a constructor: both of these take a Document as an argument. I’ve implemented the Create() function to call through to the constructor – which sets the DateTime property to the current time – but then to add an hour to the time before returning it. This lets us see that the Create() method is called if it is found (you can comment simply it out to see the behaviour when the constructor is used instead).
Otherwise the sample is pretty much the same as the one on the ADN site. The separate manager is used to associate a Document with the created instance of our custom class as well as to retrieve the object associated with a particular document. If your class derives from IDisposable then its Dispose() method will be called when the associated Document gets destroyed (this isn’t a feature we need to implement in our manager, it happens automagically).
To try the code out, NETLOAD the module that includes the code into AutoCAD and run the PPDD command in a number of different documents, both existing and new.
In the next post, we’ll take a look at swapping out the custom manager to use the built-in UserData capability.