CompileTimeValidatemethod can be overridden to make sure that it has been applied correctly in the code. What we did not do, however, was to look at the context in which these aspects can be used.
The example given previously is straight out of the source code for Jasema‘s undo feature. This sort of functionality is very, very, very useful in applications involving complex tasks such as drawing, or editing a document, but they can also be a pain to write – not only do you need to maintain a log of each action, but you also have to make sure that each log entry has a way of being rolled back. What follows is a brief description of the route we took when writing the implementation used in Jasema. It is essentially a bastardised command pattern implementation, with a fair sprinkling of AOP thrown in for extra added fun and games.
To keep track of what the user is doing, we implemented a Journal class, which is, at heart, a wrapper around two stacks. Stacks model the requirements of an undo function very well – last in, first out, so they are the most common type of collection used for this purpose. Our stacks accepts IJournalEntry types; these are objects which specify:
- A reference to the instance being modified.
- The state of the object before the change.
- The state of the object after the change.
- Instructions describing how to go back to the older state.
- Instructions describing how to go back to the newer state.
The specific implementations of IJournalEntry provide the exact instructions. This allows the undo stack to maintain information about more than one type of change. When something happens, we
push it on top of the stack; when we hit undo, we
pop the last item off, and execute its roll back instructions. To support redo functionality, what gets popped off the undo stack gets pushed on the “undone entries” stack. If it’s redone, we pop it off, execute it’s redo instruction, and push it back to the undo stack. To maintain some measure of sanity, however, we’re clearing the redo stack every time a new entry is pushed to the undo stack, since trying to figure how to sort that out gave me a headache.
Now, we have a stack which can keep track of the history of our items, and a structure (or a number of structures) to record the changes. What we need to do now is to find some way of noticing that something is happening, and write it into the journal. Rather than using events or actually writing the code to populate the entries and log them in the appropriate methods, we chose to use aspects to decorate the methods that cause the changes. If we had decided to call the journal code in the middle of another method, the code would have been pretty ugly; I’m a firm believer in separation of concern, and if a method is named “UpdatePoint”, I don’t expect 75% of the logic it contains to deal with logging. By using aspects, we can mark the methods we want to keep track of, and only write very specific code in the method itself.
In most cases, we’re using the OnEntry method of the aspect to collect state information before the method executes, and the OnExit method to collect the state, determine any changes, and log them after the method exits. The choice of OnExit as opposed to OnSuccess is deliberate. If the method fails with an exception, OnSuccess is not called; if there are any changes to the object being observed, the object may be in an inconsistent state; if we do not record this change, we cannot roll it back. Only in the case of the ListContentChangeRecorderAttribute family do we use the OnSuccess overload – the idea is that an attempt to add an item to a list will either fail or succeed, with no further side effects.
The problem with this is, of course, that the Journal exists as an instance within the application scope, while the aspect is an attribute. This means that the aspect is unable to communicate with the journal, because it does not exist before the application actually executes. One solution would have been simply to make the journal
static, but that didn’t really appeal to us, since we wanted to keep things as flexible as possible. Instead, we put together a journal provider, which is in itself static, but whose only purpose is to hold any sort of
IJournal implementation we might care to throw at it. This not only means that the aspects can now get to the journal via the provider, but also that we can now have multiple journals per application, accessible through the same provider. The journal provider is a dictionary of journals, referenced by a name. Registering a journal with the provider is as simple as:
// Create the root Journal
// (JasemaDesigner.xaml.cs, line: 99)
IJournal journal = new SimpleJournal();
IJournalProvider provider = JournalProvider.Instance; provider.Register(JournalProvider.Root, journal);
From that point on, the application only communicates with the journal in two ways: either through the aspects, for example:
// SegmentInfo.cs, line: 161
Which tells the attribute to ask the JournalProvider for the root journal. (Another override allows you to specify which journal you want to use). The second way is through the Undo() and Redo() methods, which do exactly that.
The source code described in this article can be found in the “Journal” folder in the source distribution of Jasema, which can be downloaded from the project page on C# Disciples. Any comments, suggestions, queries or casual banter appreciated