Undo Contexts
The modern user interface has come a long way toward fulfilling one of humankind's greatest hopesto be saved from its own stupidity. The undo facility you find in most of today's applications represents the crowning achievement of this pursuit. The next best thing to a time machine, undo allows you to roll back your most recent mistakesusually with considerable reliefso that you can start making new ones in their place. The automation object model gives you full access to Visual Studio .NET's undo manager, allowing you to define your own sets of mistakes that can be undone at the click of a button.
Automatic Undo Contexts
The basic unit of "undoability" is the undo context. (We'll use this term to mean both an undoable unitthe named entity that appears on the undo listand the mechanism by which you group individual actions to create an undoable unit.) The Visual Studio .NET IDE creates undo contexts automatically as you program, allowing you to undo and redo edits to your code. Try the following experiment to see some of the automatic undo contexts created by Visual Studio .NET:Open a blank text file in Visual Studio .NET.Type spelled backwards is epyT and press Enter.Copy a block of text from some document and paste it into the text file.
When you've finished, click the drop-down list on the Undo button and you'll see the list of undo contexts shown in Figure 11-4. (The drop-down list represents the document's undo stack, which is the internal data structure that stores the undoable changes.) The three undo contexts named Paste, Enter, and Type each represent one or more individual actions that can be undone as a whole. You can appreciate the ability to group multiple actions under a single name when it comes to large paste operations, where the alternative would be undoing the pasted characters one by one.
Figure 11-4. A list of undo contexts

Creating Undo Contexts
An undo context is an atomic transaction: you open the undo context and give it a name, make changes to one or more documents, and then either commit the changes by closing the undo context or abort all the changes. Once committed, the changes can be undone as a group only. You create your own undo contexts by calling methods of the DTE.UndoContext object: Open begins an undo context, SetAborted discards all changes made within the current undo context, and Close commits the changes and pushes the undo context onto the undo stacks of the participating documents.The undo manager in Visual Studio .NET allows only one undo context at a time to be open, and to share that undo context you must follow a few rules. First, always call Open within a try block because this method throws an exception if an undo context is already open. Although you can check the availability of the undo context by using the UndoContext.IsOpen property, which returns True when an undo context is open, a False value won't guarantee that the undo context will still be free by the time your code executes Open. Second, if you open an undo context, you should close it when you're finished with it by calling Close or SetAbort. (Use just one or the other because SetAbort closes the undo context for you and calling Close on a closed undo context raises an exception.) Third, you should never call SetAbort or Close on someone else's undo context. That's just not nice.ImportantMacro writers, be aware that the undo manager doesn't trust you to close your own undo contexts. Instead, it automatically closes any undo context opened by a macro when execution returns to Visual Studio .NET. (Add-ins are free to keep their undo contexts open as long as they want, regardless of the consequences.) If you find that somewhat demeaning, then don't give the undo manager the satisfactioninstead, always use SetAbort or Close to tidy up your undo contexts.Because only one undo context can be open at a time, if you don't acquire the undo context, any changes you make will belong to some other context. If the changes you need to make absolutely positively must be in their own context, you'll have to poll the UndoContext.IsOpen property until the undo context becomes free. Listing 11-1 shows one way to do this.
Listing 11-1 Grabbing the undo context when it becomes free
UndoContextTimer
<System.ContextStaticAttribute()> Public WithEvents UndoTimer _
As System.Timers.Timer
Private Const title As String = "Text Editor"
Sub UndoContextTimer()
' Description: If undo context is busy, creates a timer that
' polls the UndoContext.IsOpen property until the
' undo context becomes free
' Start timer if undo context is busy
If Not AddText() Then
UndoTimer = New System.Timers.Timer()
UndoTimer.Interval = 100
UndoTimer.Enabled = True
End If
End Sub
Private Function AddText() As Boolean
' Description: Adds text to the "Text File" document within
' the "Timer" undo context
Dim success As Boolean = True
Try
' Open the undo context
DTE.UndoContext.Open("Timer")
' Open the "Text File" document for editing
Dim txtDoc As TextDocument = GetTextDocument("Text File")
Dim editPnt As EditPoint = txtDoc.StartPoint.CreateEditPoint
' Add some text
editPnt.Insert("Here's text in the 'Timer' undo context." _
+ System.Environment.NewLine)
' Close the undo context
DTE.UndoContext.Close()
Catch
success = False
End Try
Return success
End Function
Public Sub UndoTimer_Elapsed(ByVal sender As Object, _
ByVal e As System.Timers.ElapsedEventArgs) Handles UndoTimer.Elapsed
' Check whether the undo context is free
If Not DTE.UndoContext.IsOpen Then
UndoTimer.Enabled = False
' Kill timer if AddText is successful, otherwise re-enable it
If AddText() Then
UndoTimer.Dispose()
UndoTimer = Nothing
Else
UndoTimer.Enabled = True
End If
End If
End Sub
Function GetTextDocument(ByVal caption As String) As TextDocument
' Description: Retrieves the TextDocument object associated with
' the specified window, or creates a text file in
' a new window and returns its TextDocument object
Dim doc As Document = GetDocument(caption)
Return doc.Object("TextDocument")
End Function
Function GetDocument(caption As String) As Document
' Description: Retrieves the Document object associated with
' the specified window, or creates a document in
' a new window
Dim win As Window
Try
' Check whether window is open
win = DTE.Windows.Item(caption)
Catch
' Create a new text file
win = DTE.ItemOperations.NewFile("General\Text File", caption)
End Try
Return win.Document
End Function
Normally, you shouldn't have to resort to the measures taken in Listing 11-1 because the undo context shouldn't be open for long periods of time. However, it's good to know that it can be done.
Stack Linkage
Sooner or later, when you edit multiple documents within the same undo context, you run across the problem of desynchronized undo stacks. Suppose you edit Document1 and Document2 within the Link undo context. After you close Link, it gets pushed onto the tops of the two documents' undo stacks. Then, if you undo Link in Document1, you also undo Link in Document2 because their edits belong to the same atomic operation. So far, so good.Suppose you add some text to Document2. These new edits get pushed onto the top of Document2's undo stack. What happens now when you try to undo Link in Document1? To respect Link's atomicity, you have to undo Link in Document2, and there's the problemyou can't undo Link in Document2 without first undoing the text that was just added. The undo stacks have become desynchronized.The undo manager solves this synchronization problem by introducing the concept of stack linkage. By default, an undo context that involves more than one document has a nonstrict stack linkage, which allows the atomicity of the undo context to be broken across documents; when the break happens, each document ends up with its own undo context containing only changes to itself. In our previous example, if the Link undo context were created with a nonstrict stack linkage, you could undo Link in Document1 without affecting Document2. Link would disappear from Document1's undo stack but remain on Document2's undo stack, minus the changes to Document1. A strict stack linkage, on the other hand, enforces the undo context's atomicity with extreme prejudice. If our previous example were to involve a strict stack linkage, the undo manager would kill any attempt to undo Link in Document1.You specify the strictness of the stack linkage through the second parameter to UndoContext.Open, passing True for strict. You can identify undo contexts with strict stack linkage by the plus (+) sign that precedes their names on undo lists.
Lab: Strict and Nonstrict Stack LinkageThe UndoContexts.StackLinkage macro lets you test the differences between strict and nonstrict stack linkages. This macro creates three documents and adds text to them within an undo context; an optional Boolean parameter controls whether the undo context's stack linkage is strict. Follow these steps to see a nonstrict stack linkage in action:In the Command Window, type Macros.InsideVSNET.Chapter11.UndoContexts.StackLinkage and press Enter. The macro creates three filesNonstrict1, Nonstrict2, and Nonstrict3and adds text to them within the NonstrictLinkage undo context.In any of the files, click the Undo button, and then click the Redo button. You'll see that the changes to the documents are undone and redone as a group.Add some additional text to the Nonstrict2 file.Select the Nonstrict3 file and click its Undo button. The changes disappear from Nonstrict3 and its Undo button turns gray. The undo lists for Nonstrict1 and Nonstrict2 still show NonstrictLinkage, however, which means that the atomicity of NonstrictLinkage has been broken. You'll find that Nonstrict1's NonstrictLinkage undoes the changes in Nonstrict1 without affecting Nonstrict2, and vice-versa.Now, close all the documents and redo the previous steps, but add True to the macro command in step 1. The True parameter tells StackLinkage to create files named Strict1, Strict2, and Strict3, and to add text to them within the StrictLinkage undo context. This time, when you try step 4, you'll get the error message "The application cannot undo." That's the essence of strict stack linkage. |